diff --git a/bin/scripts/unix/idea.sh b/bin/scripts/unix/idea.sh index a22209fa94ce..ec5b90e3f380 100755 --- a/bin/scripts/unix/idea.sh +++ b/bin/scripts/unix/idea.sh @@ -63,7 +63,6 @@ fi # --------------------------------------------------------------------- if [ -n "$@@product_uc@@_JDK" -a -x "$@@product_uc@@_JDK/bin/java" ]; then JDK="$@@product_uc@@_JDK" - echo "@@product_uc@@_JDK: $@@product_uc@@_JDK" fi if [ -z "$JDK" ] || [ ! -x "$JDK/bin/java" ] && @@ -72,25 +71,21 @@ if [ -z "$JDK" ] || [ ! -x "$JDK/bin/java" ] && if [ ! -d "$JDK" ]; then JDK="$IDE_HOME/$JDK" fi - echo "boot jdk: $JDK" fi if [ -z "$JDK" ] || [ ! -x "$JDK/bin/java" ] && [ "$OS_TYPE" = "Linux" ] && [ -x "$IDE_HOME/jre64/bin/java" ] && "$IDE_HOME/jre64/bin/java" -version > /dev/null 2>&1 ; then JDK="$IDE_HOME/jre64" - echo "bundled jre: $JDK" fi if [ -z "$JDK" ] || [ ! -x "$JDK/bin/java" ] && [ -n "$JDK_HOME" -a -x "$JDK_HOME/bin/java" ]; then JDK="$JDK_HOME" - echo "JDK_HOME: $JDK" fi if [ -z "$JDK" ] || [ ! -x "$JDK/bin/java" ]; then if [ -n "$JAVA_HOME" -a -x "$JAVA_HOME/bin/java" ]; then JDK="$JAVA_HOME" - echo "JAVA_HOME: $JDK" else JAVA_BIN_PATH=`which java` if [ -n "$JAVA_BIN_PATH" ]; then diff --git a/build.xml b/build.xml index 7f432c016be6..33716395f4f7 100644 --- a/build.xml +++ b/build.xml @@ -75,7 +75,7 @@ - + diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java index 1812ade79721..c0f6c45d86d3 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java @@ -226,20 +226,16 @@ public class DebuggerUtilsImpl extends DebuggerUtilsEx{ return Boolean.TRUE.equals(debugProcess.getUserData(BatchEvaluator.REMOTE_SESSION_KEY)); } - public interface SupplierThrowing { - T get() throws E; - } - - public static T suppressExceptions(SupplierThrowing supplier, T defaultValue) throws E { + public static T suppressExceptions(ThrowableComputable supplier, T defaultValue) throws E { return suppressExceptions(supplier, defaultValue, true, null); } - public static T suppressExceptions(SupplierThrowing supplier, + public static T suppressExceptions(ThrowableComputable supplier, T defaultValue, boolean ignorePCE, Class rethrow) throws E { try { - return supplier.get(); + return supplier.compute(); } catch (ProcessCanceledException e) { if (!ignorePCE) { diff --git a/java/debugger/impl/src/com/intellij/debugger/jdi/VirtualMachineProxyImpl.java b/java/debugger/impl/src/com/intellij/debugger/jdi/VirtualMachineProxyImpl.java index cb67d2b23a8d..55ebaef45f6a 100644 --- a/java/debugger/impl/src/com/intellij/debugger/jdi/VirtualMachineProxyImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/jdi/VirtualMachineProxyImpl.java @@ -25,8 +25,8 @@ import com.intellij.debugger.engine.DebugProcessImpl; import com.intellij.debugger.engine.DebuggerManagerThreadImpl; import com.intellij.debugger.engine.evaluation.EvaluateException; import com.intellij.debugger.engine.jdi.VirtualMachineProxy; -import com.intellij.debugger.impl.DebuggerUtilsImpl; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.ReflectionUtil; import com.intellij.util.ThreeState; @@ -357,13 +357,13 @@ public class VirtualMachineProxyImpl implements JdiTimer, VirtualMachineProxy { return myVirtualMachine.mirrorOf(s); } - public StringReference mirrorOfStringLiteral(String s, DebuggerUtilsImpl.SupplierThrowing generator) + public StringReference mirrorOfStringLiteral(String s, ThrowableComputable generator) throws EvaluateException { StringReference reference = myStringLiteralCache.get(s); if (reference != null && !reference.isCollected()) { return reference; } - reference = generator.get(); + reference = generator.compute(); myStringLiteralCache.put(s, reference); return reference; } diff --git a/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java b/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java index fe6612b10d08..2fa79311b8c6 100644 --- a/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java +++ b/java/execution/impl/src/com/intellij/execution/junit/JUnitUtil.java @@ -259,7 +259,7 @@ public class JUnitUtil { return aPackage != null && aPackage.getDirectories(scope).length > 0; }; - return foundCondition.value(TEST5_PACKAGE_FQN) || foundCondition.value("org.junit.platform.engine"); + return foundCondition.value(TEST5_PACKAGE_FQN); } public static boolean isTestAnnotated(final PsiMethod method) { diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java index 580244b0b882..7da422c36c5a 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java @@ -237,7 +237,7 @@ public class MarkerType { PsiMethod[] overridings = processor.toArray(PsiMethod.EMPTY_ARRAY); if (overridings.length == 0) { final PsiClass aClass = method.getContainingClass(); - if (aClass != null && FunctionalExpressionSearch.search(aClass).findFirst() != null) { + if (aClass != null && isAbstract && FunctionalExpressionSearch.search(aClass).findFirst() != null) { return "Has functional implementations"; } return null; @@ -462,17 +462,19 @@ public class MarkerType { return super.process(psiMethod); } }); - PsiClass psiClass = ReadAction.compute(myMethod::getContainingClass); - FunctionalExpressionSearch.search(psiClass).forEach(new CommonProcessors.CollectProcessor() { - @Override - public boolean process(final PsiFunctionalExpression expr) { - if (!updateComponent(expr, myRenderer.getComparator())) { - indicator.cancel(); + if (myMethod.hasModifierProperty(PsiModifier.ABSTRACT)) { + PsiClass psiClass = ReadAction.compute(myMethod::getContainingClass); + FunctionalExpressionSearch.search(psiClass).forEach(new CommonProcessors.CollectProcessor() { + @Override + public boolean process(final PsiFunctionalExpression expr) { + if (!updateComponent(expr, myRenderer.getComparator())) { + indicator.cancel(); + } + indicator.checkCanceled(); + return super.process(expr); } - indicator.checkCanceled(); - return super.process(expr); - } - }); + }); + } } } } diff --git a/java/java-impl/src/com/intellij/codeInsight/editorActions/smartEnter/MissingIfBranchesFixer.java b/java/java-impl/src/com/intellij/codeInsight/editorActions/smartEnter/MissingIfBranchesFixer.java index 96172132642a..89011af15f93 100644 --- a/java/java-impl/src/com/intellij/codeInsight/editorActions/smartEnter/MissingIfBranchesFixer.java +++ b/java/java-impl/src/com/intellij/codeInsight/editorActions/smartEnter/MissingIfBranchesFixer.java @@ -43,7 +43,7 @@ public class MissingIfBranchesFixer implements Fixer { } private static void handleBranch(@NotNull Document doc, @NotNull PsiIfStatement ifStatement, @NotNull PsiElement beforeBranch, @Nullable PsiStatement branch) { - if (branch instanceof PsiBlockStatement) return; + if (branch instanceof PsiBlockStatement || beforeBranch.textMatches(PsiKeyword.ELSE) && branch instanceof PsiIfStatement) return; boolean transformingOneLiner = branch != null && (startLine(doc, beforeBranch) == startLine(doc, branch) || startCol(doc, ifStatement) < startCol(doc, branch)); diff --git a/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf.java b/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf.java new file mode 100644 index 000000000000..cbdd02aa8689 --- /dev/null +++ b/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf.java @@ -0,0 +1,7 @@ +class Foo { + { + if (a) + else if (elsecond) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf_after.java b/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf_after.java new file mode 100644 index 000000000000..ad424e86ea47 --- /dev/null +++ b/java/java-tests/testData/codeInsight/completeStatement/BlockBeforeElseIf_after.java @@ -0,0 +1,8 @@ +class Foo { + { + if (a) { + + } else if (elsecond) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/CompleteStatementTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/CompleteStatementTest.java index 75557d713163..24fddf9288ac 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/CompleteStatementTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInsight/CompleteStatementTest.java @@ -123,6 +123,8 @@ public class CompleteStatementTest extends EditorActionTestCase { public void testElseIf() throws Exception { doTest(); } + public void testBlockBeforeElseIf() { doTest(); } + public void testIncompleteElseIf() throws Exception { doTest(); } public void testField() throws Exception { doTest(); } diff --git a/java/testFramework/src/com/intellij/codeInsight/daemon/quickFix/ActionHint.java b/java/testFramework/src/com/intellij/codeInsight/daemon/quickFix/ActionHint.java index 897d79453bd4..15d0e5636a05 100644 --- a/java/testFramework/src/com/intellij/codeInsight/daemon/quickFix/ActionHint.java +++ b/java/testFramework/src/com/intellij/codeInsight/daemon/quickFix/ActionHint.java @@ -40,9 +40,9 @@ import static org.junit.Assert.fail; * @author Tagir Valeev */ public class ActionHint { - final String myExpectedText; - final boolean myShouldPresent; - final ProblemHighlightType myHighlightType; + private final String myExpectedText; + private final boolean myShouldPresent; + private final ProblemHighlightType myHighlightType; private ActionHint(String expectedText, boolean shouldPresent, ProblemHighlightType severity) { myExpectedText = expectedText; @@ -67,7 +67,7 @@ public class ActionHint { * @return true if this ActionHint checks that some action should be present * or false if it checks that some action should be absent */ - public boolean shouldPresent() { + boolean shouldPresent() { return myShouldPresent; } diff --git a/platform/analysis-api/src/com/intellij/codeInsight/intention/IntentionActionBean.java b/platform/analysis-api/src/com/intellij/codeInsight/intention/IntentionActionBean.java index 5f08b7cd1c62..9d5a4f9c533a 100644 --- a/platform/analysis-api/src/com/intellij/codeInsight/intention/IntentionActionBean.java +++ b/platform/analysis-api/src/com/intellij/codeInsight/intention/IntentionActionBean.java @@ -22,12 +22,11 @@ import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.CustomLoadingExtensionPointBean; -import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.annotations.Tag; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Locale; import java.util.ResourceBundle; public class IntentionActionBean extends CustomLoadingExtensionPointBean { @@ -67,6 +66,7 @@ public class IntentionActionBean extends CustomLoadingExtensionPointBean { return descriptionDirectoryName; } + @NotNull public IntentionAction instantiate() throws ClassNotFoundException { return (IntentionAction)instantiateExtension(className, ApplicationManager.getApplication().getPicoContainer()); } diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/CompilationTasksImpl.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/CompilationTasksImpl.groovy index 81050a164483..8995de6c7476 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/CompilationTasksImpl.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/CompilationTasksImpl.groovy @@ -42,7 +42,6 @@ class CompilationTasksImpl extends CompilationTasks { ensureKotlinCompilerAddedToClassPath() - context.projectBuilder.cleanOutput() context.messages.progress("Compiling project") try { if (moduleNames == null) { diff --git a/platform/core-api/src/com/intellij/psi/search/DelegatingGlobalSearchScope.java b/platform/core-api/src/com/intellij/psi/search/DelegatingGlobalSearchScope.java index 7f7f05512b9c..1d2a61961d79 100644 --- a/platform/core-api/src/com/intellij/psi/search/DelegatingGlobalSearchScope.java +++ b/platform/core-api/src/com/intellij/psi/search/DelegatingGlobalSearchScope.java @@ -75,6 +75,11 @@ public class DelegatingGlobalSearchScope extends GlobalSearchScope { return myBaseScope.getDisplayName(); } + @Override + public String toString() { + return getClass().getName() + "[" + myBaseScope + "]"; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/platform/core-impl/src/com/intellij/extapi/psi/StubBasedPsiElementBase.java b/platform/core-impl/src/com/intellij/extapi/psi/StubBasedPsiElementBase.java index 0b2bb26ca9db..06f5f6092073 100644 --- a/platform/core-impl/src/com/intellij/extapi/psi/StubBasedPsiElementBase.java +++ b/platform/core-impl/src/com/intellij/extapi/psi/StubBasedPsiElementBase.java @@ -24,7 +24,6 @@ import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectCoreUtil; -import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.RecursionManager; import com.intellij.openapi.vfs.VirtualFile; @@ -367,7 +366,7 @@ public class StubBasedPsiElementBase extends ASTDelegateP @NotNull public IStubElementType getElementType() { if (!(myElementType instanceof IStubElementType)) { - throw new AssertionError("Not a stub type: " + myElementType + " in " + getClass()); + throw new ClassCastException("Not a stub type: " + myElementType + " in " + getClass()); } return (IStubElementType)myElementType; } diff --git a/platform/core-impl/src/com/intellij/util/CachedValueLeakChecker.java b/platform/core-impl/src/com/intellij/util/CachedValueLeakChecker.java index 71f7447ae1e3..0e8ddb7c5d44 100644 --- a/platform/core-impl/src/com/intellij/util/CachedValueLeakChecker.java +++ b/platform/core-impl/src/com/intellij/util/CachedValueLeakChecker.java @@ -23,6 +23,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.UserDataHolder; import com.intellij.psi.PsiElement; import com.intellij.psi.util.CachedValueProvider; @@ -42,6 +43,7 @@ class CachedValueLeakChecker { private static final Logger LOG = Logger.getInstance("#com.intellij.util.CachedValueChecker"); private static final boolean DO_CHECKS = ApplicationManager.getApplication().isUnitTestMode(); private static final Set ourCheckedKeys = ContainerUtil.newConcurrentSet(); + private static final boolean JAVA9 = SystemInfo.isJavaVersionAtLeast("9"); static void checkProvider(@NotNull final CachedValueProvider provider, @NotNull final Key key, @@ -49,7 +51,9 @@ class CachedValueLeakChecker { if (!DO_CHECKS || ApplicationInfoImpl.isInStressTest()) return; if (!ourCheckedKeys.add(key.toString())) return; // store strings because keys are created afresh in each (test) project - findReferencedPsi(provider, userDataHolder, 5); + if (!JAVA9) { + findReferencedPsi(provider, userDataHolder, 5); + } } private static synchronized void findReferencedPsi(@NotNull final Object root, diff --git a/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java b/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java index 28c2a6cd69e7..a3b77c6ffe93 100644 --- a/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java +++ b/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java @@ -15,10 +15,10 @@ */ package com.intellij.util; -import com.intellij.openapi.util.UserDataHolder; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.UserDataHolderEx; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.UserDataHolder; +import com.intellij.openapi.util.UserDataHolderEx; import com.intellij.psi.util.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/platform/diff-impl/src/com/intellij/diff/merge/TextMergeViewer.java b/platform/diff-impl/src/com/intellij/diff/merge/TextMergeViewer.java index a6ebdc48cc8a..7d1274659e29 100644 --- a/platform/diff-impl/src/com/intellij/diff/merge/TextMergeViewer.java +++ b/platform/diff-impl/src/com/intellij/diff/merge/TextMergeViewer.java @@ -36,6 +36,7 @@ import com.intellij.diff.tools.util.base.HighlightPolicy; import com.intellij.diff.tools.util.base.IgnorePolicy; import com.intellij.diff.tools.util.base.TextDiffViewerUtil; import com.intellij.diff.tools.util.text.LineOffsets; +import com.intellij.diff.tools.util.text.LineOffsetsUtil; import com.intellij.diff.tools.util.text.MergeInnerDifferences; import com.intellij.diff.tools.util.text.TextDiffProviderBase; import com.intellij.diff.util.*; @@ -406,7 +407,7 @@ public class TextMergeViewer implements MergeTool.MergeViewer { indicator.checkCanceled(); return ContainerUtil.map(contents, content -> content.getDocument().getImmutableCharSequence()); }); - List lineOffsets = ContainerUtil.map(sequences, LineOffsets::create); + List lineOffsets = ContainerUtil.map(sequences, LineOffsetsUtil::create); ComparisonManager manager = ComparisonManager.getInstance(); List lineFragments = manager.compareLines(sequences.get(0), sequences.get(1), sequences.get(2), diff --git a/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsets.java b/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsets.java index 7b952afc6f98..3c72d8fe255b 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsets.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsets.java @@ -15,61 +15,14 @@ */ package com.intellij.diff.tools.util.text; -import com.intellij.openapi.util.text.StringUtil; -import gnu.trove.TIntArrayList; -import org.jetbrains.annotations.NotNull; +public interface LineOffsets { + int getLineStart(int line); -public class LineOffsets { - private final int[] myLineEnds; - private final int myTextLength; + int getLineEnd(int line); - private LineOffsets(int[] ends, int length) { - myLineEnds = ends; - myTextLength = length; - } + int getLineNumber(int offset); - public int getLineStart(int line) { - checkLineIndex(line); - if (line == 0) return 0; - return myLineEnds[line - 1] + 1; - } + int getLineCount(); - public int getLineEnd(int line) { - checkLineIndex(line); - return myLineEnds[line]; - } - - public int getLineCount() { - return myLineEnds.length; - } - - public int getTextLength() { - return myTextLength; - } - - private void checkLineIndex(int index) { - if (index < 0 || index >= getLineCount()) { - throw new IndexOutOfBoundsException("Wrong line: " + index + ". Available lines count: " + getLineCount()); - } - } - - @NotNull - public static LineOffsets create(@NotNull CharSequence text) { - TIntArrayList ends = new TIntArrayList(); - - int index = 0; - while (true) { - int lineEnd = StringUtil.indexOf(text, '\n', index); - if (lineEnd != -1) { - ends.add(lineEnd); - index = lineEnd + 1; - } - else { - ends.add(text.length()); - break; - } - } - - return new LineOffsets(ends.toNativeArray(), text.length()); - } + int getTextLength(); } diff --git a/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsetsUtil.java b/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsetsUtil.java new file mode 100644 index 000000000000..273cce8b5aab --- /dev/null +++ b/platform/diff-impl/src/com/intellij/diff/tools/util/text/LineOffsetsUtil.java @@ -0,0 +1,130 @@ +/* + * Copyright 2000-2017 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.diff.tools.util.text; + +import com.intellij.diff.util.DiffUtil; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.text.StringUtil; +import gnu.trove.TIntArrayList; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +public class LineOffsetsUtil { + public static LineOffsets create(@NotNull Document document) { + return new LineOffsetsDocumentWrapper(document); + } + + @NotNull + public static LineOffsets create(@NotNull CharSequence text) { + TIntArrayList ends = new TIntArrayList(); + + int index = 0; + while (true) { + int lineEnd = StringUtil.indexOf(text, '\n', index); + if (lineEnd != -1) { + ends.add(lineEnd); + index = lineEnd + 1; + } + else { + ends.add(text.length()); + break; + } + } + + return new LineOffsetsImpl(ends.toNativeArray(), text.length()); + } + + private static class LineOffsetsImpl implements LineOffsets { + private final int[] myLineEnds; + private final int myTextLength; + + private LineOffsetsImpl(int[] lineEnds, int textLength) { + myLineEnds = lineEnds; + myTextLength = textLength; + } + + public int getLineStart(int line) { + checkLineIndex(line); + if (line == 0) return 0; + return myLineEnds[line - 1] + 1; + } + + public int getLineEnd(int line) { + checkLineIndex(line); + return myLineEnds[line]; + } + + @Override + public int getLineNumber(int offset) { + if (offset < 0 || offset > getTextLength()) { + throw new IndexOutOfBoundsException("Wrong offset: " + offset + ". Available text length: " + getTextLength()); + } + if (offset == 0) return 0; + if (offset == getTextLength()) return getLineCount() - 1; + + int bsResult = Arrays.binarySearch(myLineEnds, offset); + return bsResult >= 0 ? bsResult : -bsResult - 1; + } + + public int getLineCount() { + return myLineEnds.length; + } + + public int getTextLength() { + return myTextLength; + } + + private void checkLineIndex(int index) { + if (index < 0 || index >= getLineCount()) { + throw new IndexOutOfBoundsException("Wrong line: " + index + ". Available lines count: " + getLineCount()); + } + } + } + + private static class LineOffsetsDocumentWrapper implements LineOffsets { + @NotNull private final Document myDocument; + + public LineOffsetsDocumentWrapper(@NotNull Document document) { + myDocument = document; + } + + @Override + public int getLineStart(int line) { + return myDocument.getLineStartOffset(line); + } + + @Override + public int getLineEnd(int line) { + return myDocument.getLineEndOffset(line); + } + + @Override + public int getLineNumber(int offset) { + return myDocument.getLineNumber(offset); + } + + @Override + public int getLineCount() { + return DiffUtil.getLineCount(myDocument); + } + + @Override + public int getTextLength() { + return myDocument.getTextLength(); + } + } +} diff --git a/platform/diff-impl/src/com/intellij/diff/tools/util/text/SimpleThreesideTextDiffProvider.java b/platform/diff-impl/src/com/intellij/diff/tools/util/text/SimpleThreesideTextDiffProvider.java index 3fffcdc1e4e6..779174cd4bc2 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/util/text/SimpleThreesideTextDiffProvider.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/util/text/SimpleThreesideTextDiffProvider.java @@ -57,7 +57,7 @@ public class SimpleThreesideTextDiffProvider extends TextDiffProviderBase { ComparisonPolicy comparisonPolicy = ignorePolicy.getComparisonPolicy(); List sequences = ContainerUtil.list(text1, text2, text3); - List lineOffsets = ContainerUtil.map(sequences, LineOffsets::create); + List lineOffsets = ContainerUtil.map(sequences, LineOffsetsUtil::create); indicator.checkCanceled(); List lineFragments = ComparisonManager.getInstance().compareLines(text1, text2, text3, comparisonPolicy, indicator); diff --git a/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java b/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java index 18c5915c1a7e..05257705b957 100644 --- a/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java +++ b/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java @@ -858,16 +858,7 @@ public class DiffUtil { @NotNull public static TextRange getLinesRange(@NotNull Document document, int line1, int line2, boolean includeNewline) { - if (line1 == line2) { - int lineStartOffset = line1 < getLineCount(document) ? document.getLineStartOffset(line1) : document.getTextLength(); - return new TextRange(lineStartOffset, lineStartOffset); - } - else { - int startOffset = document.getLineStartOffset(line1); - int endOffset = document.getLineEndOffset(line2 - 1); - if (includeNewline && endOffset < document.getTextLength()) endOffset++; - return new TextRange(startOffset, endOffset); - } + return getLinesRange(LineOffsetsUtil.create(document), line1, line2, includeNewline); } @NotNull diff --git a/platform/diff-impl/tests/com/intellij/diff/util/LineOffsetsTest.kt b/platform/diff-impl/tests/com/intellij/diff/util/LineOffsetsTest.kt index dad6cc51773a..cd698d0db24a 100644 --- a/platform/diff-impl/tests/com/intellij/diff/util/LineOffsetsTest.kt +++ b/platform/diff-impl/tests/com/intellij/diff/util/LineOffsetsTest.kt @@ -16,7 +16,7 @@ package com.intellij.diff.util import com.intellij.diff.DiffTestCase -import com.intellij.diff.tools.util.text.LineOffsets +import com.intellij.diff.tools.util.text.LineOffsetsUtil import com.intellij.openapi.editor.impl.DocumentImpl class LineOffsetsTest : DiffTestCase() { @@ -40,26 +40,34 @@ class LineOffsetsTest : DiffTestCase() { } private fun checkSameAsDocument(text: String) { - val lineOffsets = LineOffsets.create(text) - val document = DocumentImpl(text) + val lineOffsets1 = LineOffsetsUtil.create(DocumentImpl(text)) + val lineOffsets2 = LineOffsetsUtil.create(text) - assertEquals(lineOffsets.lineCount, getLineCount(document)) - assertEquals(lineOffsets.textLength, document.textLength) + assertEquals(lineOffsets1.lineCount, lineOffsets2.lineCount) + assertEquals(lineOffsets1.textLength, lineOffsets2.textLength) - for (i in 0 until lineOffsets.lineCount) { - assertEquals(lineOffsets.getLineStart(i), document.getLineStartOffset(i)) - assertEquals(lineOffsets.getLineEnd(i), document.getLineEndOffset(i)) + for (i in 0 until lineOffsets1.lineCount) { + assertEquals(lineOffsets1.getLineStart(i), lineOffsets2.getLineStart(i)) + assertEquals(lineOffsets1.getLineEnd(i), lineOffsets2.getLineEnd(i)) + } + + for (i in 0..lineOffsets1.textLength) { + assertEquals(lineOffsets1.getLineNumber(i), lineOffsets2.getLineNumber(i)) } } private fun checkOffsets(text: String, vararg offsets: IntPair) { - val lineOffsets = LineOffsets.create(text) + val lineOffsets = LineOffsetsUtil.create(text) assertEquals(offsets.size, lineOffsets.lineCount) - offsets.forEachIndexed { i, value -> - assertEquals(lineOffsets.getLineStart(i), value.val1) - assertEquals(lineOffsets.getLineEnd(i), value.val2) + offsets.forEachIndexed { line, value -> + assertEquals(lineOffsets.getLineStart(line), value.val1) + assertEquals(lineOffsets.getLineEnd(line), value.val2) + + for (offset in lineOffsets.getLineStart(line)..lineOffsets.getLineEnd(line)) { + assertEquals(line, lineOffsets.getLineNumber(offset)) + } } } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchType.java b/platform/dvcs-impl/src/com/intellij/dvcs/branch/BranchType.java similarity index 75% rename from plugins/git4idea/src/git4idea/branch/GitBranchType.java rename to platform/dvcs-impl/src/com/intellij/dvcs/branch/BranchType.java index a6e6fa641451..fda711b0a633 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchType.java +++ b/platform/dvcs-impl/src/com/intellij/dvcs/branch/BranchType.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package git4idea.branch; +package com.intellij.dvcs.branch; -public enum GitBranchType { - LOCAL, REMOTE +import org.jetbrains.annotations.NotNull; + +public interface BranchType { + + @NotNull + String getName(); } diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchManager.java b/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchManager.java new file mode 100644 index 000000000000..c9af3ae983b9 --- /dev/null +++ b/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchManager.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2017 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.dvcs.branch; + +import com.intellij.dvcs.repo.AbstractRepositoryManager; +import com.intellij.dvcs.repo.Repository; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +import static com.intellij.util.containers.ContainerUtil.map2List; +import static com.intellij.util.containers.ContainerUtil.newArrayList; + +public abstract class DvcsBranchManager { + @NotNull private final AbstractRepositoryManager myRepositoryManager; + @NotNull private final DvcsBranchSettings myBranchSettings; + @NotNull public final BranchStorage myPredefinedFavoriteBranches = new BranchStorage(); + + protected DvcsBranchManager(@NotNull AbstractRepositoryManager repositoryManager, + @NotNull DvcsBranchSettings settings, + @NotNull BranchType[] branchTypes) { + myRepositoryManager = repositoryManager; + myBranchSettings = settings; + for (BranchType type : branchTypes) { + String defaultBranchName = getDefaultBranchName(type); + if (!StringUtil.isEmptyOrSpaces(defaultBranchName)) { + myPredefinedFavoriteBranches.myBranches.put(type.getName(), constructDefaultBranchPredefinedList(defaultBranchName)); + } + } + } + + @NotNull + private List constructDefaultBranchPredefinedList(@NotNull String defaultBranchName) { + List branchInfos = newArrayList(new DvcsBranchInfo("", defaultBranchName)); + branchInfos.addAll(map2List(myRepositoryManager.getRepositories(), + repository -> new DvcsBranchInfo(repository.getRoot().getPath(), defaultBranchName))); + return branchInfos; + } + + @Nullable + protected String getDefaultBranchName(@NotNull BranchType type) {return null;} + + public boolean isFavorite(@Nullable BranchType branchType, @Nullable Repository repository, @NotNull String branchName) { + if (branchType == null) return false; + String branchTypeName = branchType.getName(); + if (myBranchSettings.getFavorites().contains(branchTypeName, repository, branchName)) return true; + if (myBranchSettings.getExcludedFavorites().contains(branchTypeName, repository, branchName)) return false; + return myPredefinedFavoriteBranches.contains(branchTypeName, repository, branchName); + } + + public void setFavorite(@Nullable BranchType branchType, + @Nullable Repository repository, + @NotNull String branchName, + boolean shouldBeFavorite) { + if (branchType == null) return; + String branchTypeName = branchType.getName(); + if (shouldBeFavorite) { + myBranchSettings.getFavorites().add(branchTypeName, repository, branchName); + myBranchSettings.getExcludedFavorites().remove(branchTypeName, repository, branchName); + } + else { + if (myBranchSettings.getFavorites().contains(branchTypeName, repository, branchName)) { + myBranchSettings.getFavorites().remove(branchTypeName, repository, branchName); + } + else if (myPredefinedFavoriteBranches.contains(branchTypeName, repository, branchName)) { + myBranchSettings.getExcludedFavorites().add(branchTypeName, repository, branchName); + } + } + } +} diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchSettings.java b/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchSettings.java new file mode 100644 index 000000000000..cc0c3cb02376 --- /dev/null +++ b/platform/dvcs-impl/src/com/intellij/dvcs/branch/DvcsBranchSettings.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2017 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.dvcs.branch; + +import com.intellij.util.xmlb.annotations.Tag; +import org.jetbrains.annotations.NotNull; + +public class DvcsBranchSettings { + @Tag("favorite-branches") + private BranchStorage myFavoriteBranches = new BranchStorage(); + @Tag("excluded-from-favorite") + private BranchStorage myExcludedFavorites = new BranchStorage(); + + @NotNull + public BranchStorage getFavorites() { + return myFavoriteBranches; + } + + @NotNull + public BranchStorage getExcludedFavorites() { + return myExcludedFavorites; + } +} diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/project/manage/ExternalProjectsManager.java b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/project/manage/ExternalProjectsManager.java index e6472e0f49a3..aea2cfc34737 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/project/manage/ExternalProjectsManager.java +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/project/manage/ExternalProjectsManager.java @@ -58,7 +58,7 @@ public class ExternalProjectsManager implements PersistentStateComponent { private Area myCurArea; private final List myAreas; private final HackSearch myHackSearch; - // EA-28497, EA-26379 - private final Getter myDebugDocumentTextGetter; public StepIntersection(Convertor dataConvertor, Convertor areasConvertor, - final List areas, - Getter debugDocumentTextGetter) { + final List areas) { myAreas = areas; - myDebugDocumentTextGetter = debugDocumentTextGetter; myAreaIndex = 0; myDataConvertor = dataConvertor; myAreasConvertor = areasConvertor; @@ -59,10 +53,6 @@ public class StepIntersection { (o1, o2) -> o1.intersects(o2) ? 0 : o1.getStartOffset() < o2.getStartOffset() ? -1 : 1); } - public void resetIndex() { - myAreaIndex = 0; - } - public List process(final Iterable data) { final List result = new ArrayList<>(); process(data, (data1, area) -> result.add(data1)); diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleMainPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleMainPanel.java index 8f715fd04cc5..de4f41a63d35 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleMainPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleMainPanel.java @@ -18,11 +18,14 @@ package com.intellij.application.options.codeStyle; import com.intellij.application.options.CodeStyleAbstractPanel; import com.intellij.application.options.TabbedLanguageCodeStylePanel; +import com.intellij.ide.DataManager; import com.intellij.ide.util.PropertiesComponent; import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.ex.Settings; import com.intellij.psi.codeStyle.CodeStyleScheme; import com.intellij.psi.codeStyle.CodeStyleSchemes; import com.intellij.ui.components.labels.SwingActionLink; @@ -103,7 +106,12 @@ public class CodeStyleMainPanel extends JPanel implements TabbedLanguageCodeStyl @Override public void afterCurrentSettingsChanged() { - mySchemesPanel.updateOnCurrentSettingsChange(); + mySchemesPanel.updateOnCurrentSettingsChange(); + DataContext context = DataManager.getInstance().getDataContext(mySettingsPanel); + Settings settings = Settings.KEY.getData(context); + if (settings != null) { + settings.revalidate(); + } } @Override diff --git a/platform/lang-impl/src/com/intellij/application/options/colors/ColorSchemeActions.java b/platform/lang-impl/src/com/intellij/application/options/colors/ColorSchemeActions.java index 20f8de4e2a88..0f89dc4ef762 100644 --- a/platform/lang-impl/src/com/intellij/application/options/colors/ColorSchemeActions.java +++ b/platform/lang-impl/src/com/intellij/application/options/colors/ColorSchemeActions.java @@ -76,10 +76,16 @@ public abstract class ColorSchemeActions extends AbstractSchemeActions visibleHighlights = getVisibleHighlights(myStartOffset, myEndOffset, myProject, myEditor); diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java index 5b5509084a38..7526c62103e4 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java @@ -66,8 +66,6 @@ import org.jetbrains.annotations.TestOnly; import javax.swing.*; import javax.swing.border.Border; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; @@ -142,12 +140,12 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { } @NotNull - public static IntentionHintComponent showIntentionHint(@NotNull final Project project, - @NotNull PsiFile file, - @NotNull final Editor editor, - @NotNull ShowIntentionsPass.IntentionsInfo intentions, - boolean showExpanded, - @NotNull Point position) { + private static IntentionHintComponent showIntentionHint(@NotNull final Project project, + @NotNull PsiFile file, + @NotNull final Editor editor, + @NotNull ShowIntentionsPass.IntentionsInfo intentions, + boolean showExpanded, + @NotNull Point position) { ApplicationManager.getApplication().assertIsDispatchThread(); final IntentionHintComponent component = new IntentionHintComponent(project, file, editor, intentions); @@ -466,31 +464,28 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { myPopupShown = false; } }); - myPopup.addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(@NotNull ListSelectionEvent e) { - final Object source = e.getSource(); - highlighter.dropHighlight(); - injectionHighlighter.dropHighlight(); - - if (source instanceof DataProvider) { - final Object selectedItem = PlatformDataKeys.SELECTED_ITEM.getData((DataProvider)source); - if (selectedItem instanceof IntentionActionWithTextCaching) { - final IntentionAction action = ((IntentionActionWithTextCaching)selectedItem).getAction(); - if (action instanceof SuppressIntentionActionFromFix) { - if (injectedFile != null && ((SuppressIntentionActionFromFix)action).isShouldBeAppliedToInjectionHost() == ThreeState.NO) { - final PsiElement at = injectedFile.findElementAt(injectedEditor.getCaretModel().getOffset()); - final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at); - if (container != null) { - injectionHighlighter.highlight(container, Collections.singletonList(container)); - } + myPopup.addListSelectionListener(e -> { + final Object source = e.getSource(); + highlighter.dropHighlight(); + injectionHighlighter.dropHighlight(); + + if (source instanceof DataProvider) { + final Object selectedItem = PlatformDataKeys.SELECTED_ITEM.getData((DataProvider)source); + if (selectedItem instanceof IntentionActionWithTextCaching) { + final IntentionAction action = ((IntentionActionWithTextCaching)selectedItem).getAction(); + if (action instanceof SuppressIntentionActionFromFix) { + if (injectedFile != null && ((SuppressIntentionActionFromFix)action).isShouldBeAppliedToInjectionHost() == ThreeState.NO) { + final PsiElement at = injectedFile.findElementAt(injectedEditor.getCaretModel().getOffset()); + final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at); + if (container != null) { + injectionHighlighter.highlight(container, Collections.singletonList(container)); } - else { - final PsiElement at = myFile.findElementAt(myEditor.getCaretModel().getOffset()); - final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at); - if (container != null) { - highlighter.highlight(container, Collections.singletonList(container)); - } + } + else { + final PsiElement at = myFile.findElementAt(myEditor.getCaretModel().getOffset()); + final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at); + if (container != null) { + highlighter.highlight(container, Collections.singletonList(container)); } } } @@ -515,12 +510,7 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { } Disposer.register(this, myPopup); - Disposer.register(myPopup, new Disposable() { - @Override - public void dispose() { - ApplicationManager.getApplication().assertIsDispatchThread(); - } - }); + Disposer.register(myPopup, ApplicationManager.getApplication()::assertIsDispatchThread); } void canceled(@NotNull ListPopupStep intentionListStep) { @@ -592,9 +582,7 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { @Override @NotNull public String getText() { - return mySettings.isEnabled(myAction) ? - CodeInsightBundle.message("disable.intention.action", myFamilyName) : - CodeInsightBundle.message("enable.intention.action", myFamilyName); + return CodeInsightBundle.message(mySettings.isEnabled(myAction) ? "disable.intention.action" : "enable.intention.action", myFamilyName); } @Override diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionListStep.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionListStep.java index 08b781cb3197..6276b4ef8271 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionListStep.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionListStep.java @@ -100,12 +100,12 @@ public class IntentionListStep implements ListPopupStep { HintManager.getInstance().hideAllHints(); if (myProject.isDisposed() || myEditor != null && myEditor.isDisposed()) return; diff --git a/platform/lang-impl/src/com/intellij/diagnostic/logging/LogConsoleBase.java b/platform/lang-impl/src/com/intellij/diagnostic/logging/LogConsoleBase.java index 7774d5955078..0730017f2234 100644 --- a/platform/lang-impl/src/com/intellij/diagnostic/logging/LogConsoleBase.java +++ b/platform/lang-impl/src/com/intellij/diagnostic/logging/LogConsoleBase.java @@ -27,6 +27,7 @@ import com.intellij.execution.ui.ConsoleView; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; @@ -303,8 +304,9 @@ public abstract class LogConsoleBase extends AdditionalTabComponent implements L else { try { final BufferedReader reader = readerThread.myReader; - while (reader != null && reader.ready()) { - addMessage(reader.readLine()); + while (reader.ready()) { + //ensure have read lock before requiring for sync, otherwise dispose() under write action would lead to deadlock + ReadAction.run(() -> addMessage(reader.readLine())); } } catch (IOException ignore) {} @@ -314,6 +316,7 @@ public abstract class LogConsoleBase extends AdditionalTabComponent implements L } protected synchronized void addMessage(final String text) { + if (myDisposed) return; if (text == null) return; if (myContentPreprocessor != null) { final List fragments = myContentPreprocessor.parseLogLine(text + "\n"); diff --git a/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewPanel.java b/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewPanel.java index a8f932379746..4cf6390f19c0 100644 --- a/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewPanel.java +++ b/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewPanel.java @@ -51,6 +51,7 @@ import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.awt.*; +import java.util.Arrays; import java.util.List; import static com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings; @@ -108,8 +109,7 @@ class DiffPreviewPanel implements PreviewPanel { private final List myContents; public SampleRequest() { - com.intellij.openapi.diff.DiffContent[] contents = DiffPreviewProvider.getContents(); - myContents = ContainerUtil.list(convert(contents[0]), convert(contents[1]), convert(contents[2])); + myContents = Arrays.asList(DiffPreviewProvider.getContents()); } private static DiffContent convert(@NotNull com.intellij.openapi.diff.DiffContent content) { diff --git a/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewProvider.java b/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewProvider.java index 327f99ea60a0..6ad6cb479202 100644 --- a/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewProvider.java +++ b/platform/lang-impl/src/com/intellij/openapi/diff/impl/settings/DiffPreviewProvider.java @@ -16,12 +16,14 @@ package com.intellij.openapi.diff.impl.settings; -import com.intellij.openapi.diff.DiffContent; -import com.intellij.openapi.diff.SimpleContent; +import com.intellij.diff.DiffContentFactory; +import com.intellij.diff.contents.DiffContent; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.StdFileTypes; import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; /** * @author oleg @@ -30,19 +32,33 @@ import org.jetbrains.annotations.NonNls; public abstract class DiffPreviewProvider { public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.diffPreviewProvider"); + @NotNull public abstract DiffContent[] createContents(); + @NotNull public static DiffContent[] getContents() { // Assuming that standalone IDE should provide one provider final DiffPreviewProvider[] providers = Extensions.getExtensions(EP_NAME); - if (providers.length != 0){ + if (providers.length != 0) { return providers[0].createContents(); } - return new DiffContent[]{createContent(LEFT_TEXT), createContent(CENTER_TEXT), createContent(RIGHT_TEXT)}; + return createContent(LEFT_TEXT, CENTER_TEXT, RIGHT_TEXT, StdFileTypes.JAVA); } - private static SimpleContent createContent(String text) { - return new SimpleContent(text, StdFileTypes.JAVA); + @NotNull + public static DiffContent[] createContent(@NotNull String left, + @NotNull String center, + @NotNull String right, + @NotNull FileType fileType) { + return new DiffContent[]{ + createContent(left, StdFileTypes.JAVA), + createContent(center, StdFileTypes.JAVA), + createContent(right, StdFileTypes.JAVA)}; + } + + @NotNull + private static DiffContent createContent(@NotNull String text, @NotNull FileType fileType) { + return DiffContentFactory.getInstance().create(text, fileType); } @NonNls private static final String LEFT_TEXT = "class MyClass {\n" + diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index 8af1eb33f1a8..d3905ab5211f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -4265,7 +4265,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi editor.putUserData(LAST_PASTED_REGION, null); EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); - LOG.assertTrue(pasteHandler instanceof EditorTextInsertHandler); + LOG.assertTrue(pasteHandler instanceof EditorTextInsertHandler, String.valueOf(pasteHandler)); ((EditorTextInsertHandler)pasteHandler).execute(editor, editor.getDataContext(), () -> t); TextRange range = editor.getUserData(LAST_PASTED_REGION); diff --git a/platform/platform-impl/src/com/intellij/openapi/options/ex/Settings.java b/platform/platform-impl/src/com/intellij/openapi/options/ex/Settings.java index 5b05bb8c4eaf..829f2e2dd63a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/ex/Settings.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/ex/Settings.java @@ -69,4 +69,9 @@ public abstract class Settings { private static Configurable choose(Configurable configurable, Configurable variant) { return variant != null ? variant : configurable; } + + /** + * Used to handle programmatic settings changes when no UI events are sent. + */ + public void revalidate() {} } diff --git a/platform/platform-impl/src/com/intellij/openapi/options/newEditor/ConfigurableEditor.java b/platform/platform-impl/src/com/intellij/openapi/options/newEditor/ConfigurableEditor.java index d9b8cdee50ac..f6d0d8efb31a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/newEditor/ConfigurableEditor.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/newEditor/ConfigurableEditor.java @@ -187,7 +187,7 @@ class ConfigurableEditor extends AbstractEditor implements AnActionListener, AWT } } - private void requestUpdate() { + void requestUpdate() { final Configurable configurable = myConfigurable; myQueue.queue(new Update(this) { @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/options/newEditor/SettingsEditor.java b/platform/platform-impl/src/com/intellij/openapi/options/newEditor/SettingsEditor.java index e828aa129760..64ba5a177a92 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/newEditor/SettingsEditor.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/newEditor/SettingsEditor.java @@ -76,6 +76,11 @@ final class SettingsEditor extends AbstractEditor implements DataProvider { myFilter.update(null, false, true); return myTreeView.select(configurable); } + + @Override + public void revalidate() { + myEditor.requestUpdate(); + } }; mySearch = new SettingsSearch() { @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/vcs/changes/TodoForRanges.java b/platform/platform-impl/src/com/intellij/openapi/vcs/changes/TodoForRanges.java index f29de9d8e075..cf526a9c6bc1 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vcs/changes/TodoForRanges.java +++ b/platform/platform-impl/src/com/intellij/openapi/vcs/changes/TodoForRanges.java @@ -20,7 +20,6 @@ import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.Getter; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vcs.checkin.StepIntersection; @@ -74,8 +73,7 @@ public abstract class TodoForRanges { public TextRange convert(TodoItemData o) { return o.getTextRange(); } - }, Convertor.SELF, myRanges, () -> "" - ); + }, Convertor.SELF, myRanges); final List filtered = stepIntersection.process(Arrays.asList(todoItems)); final List> result = new ArrayList<>(filtered.size()); int offset = 0; diff --git a/platform/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index 7eeb6203ca41..e23c2cba26ec 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -782,5 +782,7 @@ settings.editor.scheme.copy.to.ide.title=Copy Project Scheme to IDE settings.editor.scheme.copy.to.ide.label=IDE Scheme Name: settings.editor.scheme.copy.to.project.title=Copy Setting to Project settings.editor.scheme.copy.to.project.message=Overwrite project settings with values from {0}? +settings.editor.scheme.import.success={0} was imported to {1} scheme. +settings.editor.scheme.import.failure=Import failed: {0} is not a valid scheme. title.save.code.style.scheme.as=Duplicate Code Style Scheme As title.save.color.scheme.as=Duplicate Color Scheme As \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 44087d58a1a5..ba3cf23629dc 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -45,6 +45,7 @@ serviceImplementation="com.intellij.execution.filters.TextConsoleBuilderFactoryImpl"/> + diff --git a/platform/platform-tests/testData/diff/HugeFile.txt b/platform/platform-tests/testData/diff/HugeFile.txt new file mode 100644 index 000000000000..090cc55c8597 --- /dev/null +++ b/platform/platform-tests/testData/diff/HugeFile.txt @@ -0,0 +1,146496 @@ +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} + package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.ide.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataConstants; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.ex.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.EmptyHighlighter; +import com.intellij.openapi.editor.impl.event.MarkupModelEvent; +import com.intellij.openapi.editor.impl.event.MarkupModelListener; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.util.IJSwingUtilities; +import com.intellij.util.Key; +import com.intellij.progress.ProgressManager; +import org.jdom.Element; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.*; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.TooManyListenersException; + +public class EditorImpl implements EditorEx { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); + private static final Key DND_COMMAND_KEY = new Key("DndCommand"); + private final DocumentImpl myDocument; + + private JPanel myPanel; + private JScrollPane myScrollPane; + private EditorComponentImpl myEditorComponent; + private EditorGutterComponentImpl myGutterComponent; + + private Dimension mySize = null; + private CommandProcessor myCommandProcessor; + private MyScrollBar myVerticalScrollBar; + private MyScrollBar myHorizontalScrollBar; + + private ArrayList myMouseListeners = new ArrayList(); + private ArrayList myMouseMotionListeners = new ArrayList(); + + private int myCharHeight = -1; + private int myLineHeight = -1; + private int myDescent = -1; + + private boolean isInsertMode = true; + + private final CaretCursor myCaretCursor; + private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); + + private final Object MOUSE_DRAGGED_GROUP = new Key("MouseDraggedGroup"); + private final Hashtable myUserDataMap = new Hashtable(); + + private final DocumentListener myEditorDocumentAdapter; + + private EditorSettings mySettings; + + private boolean isReleased = false; + + private MouseEvent myMousePressedEvent = null; + + private int mySavedSelectionStart = -1; + private int mySavedSelectionEnd = -1; + private int myLastColumnNumber = 0; + + private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); + private MyEditable myEditable; + + private EditorColorsScheme myScheme; + private final boolean myIsViewer; + private final SelectionModelImpl mySelectionModel; + private final EditorMarkupModelImpl myMarkupModel; + private final FoldingModelImpl myFoldingModel; + private final ScrollingModelImpl myScrollingModel; + private final CaretModelImpl myCaretModel; + + private static final RepaintCursorThread ourCaretThread; + private int myBorderStart; + private int myBorderEnd; + private int myBorderY; + + private Color myBorderColor; + + private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; + private FoldRegion myMouseSelectedRegion = null; + + private final static int MOUSE_SELECTION_STATE_NONE = 0; + private final static int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; + private final static int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; + + private final MarkupModelListener myMarkupModelListener; + + private Highlighter myHighlighter; + + private int myScrollbarOrientation; + private boolean myMousePressedInsideSelection; + private FontMetrics myPlainFontMetrics; + private FontMetrics myBoldFontMetrics; + private FontMetrics myItalicFontMetrics; + private FontMetrics myBoldItalicFontMetrics; + + private static final int CACHED_CHARS_BUFFER_SIZE = 300; + + private CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN); + private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD); + private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC); + private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC); + + private int myCurrentFontType = Font.PLAIN; + + private boolean myIsBlockSelectionMode; + private int myLongestLine; + private int myLongestLineLength; + private Runnable myCursorUpdater; + private Dimension myContentSize; + private int myCaretUpdateVShift; + Project myProject; + private long myMouseSelectionChangeTimestamp; + private int mySavedCaretOffsetForDNDUndoHack; + private ArrayList myFocusListeners = new ArrayList(); + + private MyInputMethodHandler myInputMethodRequestsHandler; + private InputMethodRequests myInputMethodRequestsSwingWrapper; + private boolean myIsOneLineMode; + + static { + ourCaretThread = new RepaintCursorThread(); + ourCaretThread.start(); + } + + public EditorImpl(DocumentEx document, boolean viewer, Project project) { + myProject = project; + myDocument = (DocumentImpl) document; + myScheme = new MyColorSchemeDelegate(); + myIsViewer = viewer; + mySettings = new SettingsImpl(this); + + mySelectionModel = new SelectionModelImpl(this); + myMarkupModel = new EditorMarkupModelImpl(this); + myFoldingModel = new FoldingModelImpl(this); + myCaretModel = new CaretModelImpl(this); + myIsBlockSelectionMode = false; + myLongestLine = -1; + myLongestLineLength = -1; + + myCommandProcessor = CommandProcessor.getInstance(); + + myEditorDocumentAdapter = new EditorDocumentAdapter(); + + myMarkupModelListener = new MarkupModelListener() { + public void rangeHighlighterChanged(MarkupModelEvent event) { + RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl) event.getHighlighter(); + if (rangeHighlighter.isValid()) { + repaint(rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset()); + } else { + repaint(0, getDocument().getTextLength()); + } + ((EditorMarkupModelImpl) getMarkupModel()).repaint(); + ((EditorMarkupModelImpl) getMarkupModel()).markDirtied(); + GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer(); + if (renderer != null) { + myGutterComponent.updateSize(); + } + updateCaretCursor(); + } + }; + + ((MarkupModelImpl) myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener); + ((MarkupModelImpl) getMarkupModel()).addMarkupModelListener(myMarkupModelListener); + + myDocument.addDocumentListener(myFoldingModel); + myDocument.addDocumentListener(myCaretModel); + myDocument.addDocumentListener(mySelectionModel); + myDocument.addDocumentListener(myEditorDocumentAdapter); + + myCaretCursor = new CaretCursor(); + + myFoldingModel.flushCaretShift(); + myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT; + + initComponent(); + + Highlighter highlighter = new EmptyHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); + setHighlighter(highlighter); + + myScrollingModel = new ScrollingModelImpl(this); + + myGutterComponent.updateSize(); + validateSize(); + } + + public boolean isViewer() { + return myIsViewer; + } + + public SelectionModel getSelectionModel() { + return mySelectionModel; + } + + public MarkupModel getMarkupModel() { + return myMarkupModel; + } + + public FoldingModel getFoldingModel() { + return myFoldingModel; + } + + public CaretModel getCaretModel() { + return myCaretModel; + } + + public ScrollingModel getScrollingModel() { + return myScrollingModel; + } + + public EditorSettings getSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return mySettings; + } + + public void reinitSettings() { + ApplicationManager.getApplication().assertIsDispatchThread(); + myCharHeight = -1; + myLineHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + recalcSize(); + myFoldingModel.refreshSettings(); + myFoldingModel.rebuild(); + + if (myHighlighter instanceof EmptyHighlighter) { + ((EmptyHighlighter)myHighlighter).setAttributes(myScheme.getAttributes(HighlighterColors.TEXT)); + } + + myGutterComponent.revalidate(); + myEditorComponent.repaint(); + + updateCaretCursor(); + } + + public void release() { + isReleased = true; + myDocument.removeDocumentListener(myHighlighter); + myDocument.removeDocumentListener(myEditorDocumentAdapter); + myDocument.removeDocumentListener(myFoldingModel); + myDocument.removeDocumentListener(myCaretModel); + myDocument.removeDocumentListener(mySelectionModel); + MarkupModelImpl markupModel = (MarkupModelImpl) myDocument.getMarkupModel(myProject, false); + if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener); + myMarkupModel.dispose(); + myLineHeight = -1; + myCharHeight = -1; + myDescent = -1; + myPlainFontMetrics = null; + } + + public void putUserData(Object key, Object value) { + if (value != null){ + myUserDataMap.put(key, value); + } + else{ + myUserDataMap.remove(key); + } + } + + public Object getUserData(Object key) { + return myUserDataMap.get(key); + } + + private void initComponent() { + myEditorComponent = new EditorComponentImpl(this); +// myStatusBar = new EditorStatusBarImpl(); + + myScrollPane = new JScrollPane(); + myPanel = new JPanel() { + public void addNotify() { + super.addNotify(); + final JComponent parent = (JComponent)getParent(); + if (parent.getBorder() != null) myScrollPane.setBorder(null); + } + }; + //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS)); + myPanel.setLayout(new BorderLayout()); + + myVerticalScrollBar= new MyScrollBar(JScrollBar.VERTICAL); + myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL); + // + myScrollPane.setVerticalScrollBar(myVerticalScrollBar); + myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar); + myScrollPane.setViewportView(myEditorComponent); + //myScrollPane.setBorder(null); + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + + myGutterComponent = new EditorGutterComponentImpl(this); + myGutterComponent.setOpaque(true); + myScrollPane.setRowHeaderView(myGutterComponent); + stopOptimizedScrolling(); + + myEditorComponent.setTransferHandler(new MyTransferHandler()); + myEditorComponent.setAutoscrolls(true); + +/* Default mode till 1.4.0 + * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); + */ + myPanel.add(myScrollPane); + + myEditorComponent.addKeyListener ( + new KeyAdapter() { + public void keyPressed(KeyEvent e) { + } + + public void keyTyped(KeyEvent event) { + if(event.isConsumed()){ + return; + } + char c = event.getKeyChar(); + int modifiers = event.getModifiers(); + if (((modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK)) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + ); + + MyMouseAdapter mouseAdapter = new MyMouseAdapter(); + myEditorComponent.addMouseListener(mouseAdapter); + myGutterComponent.addMouseListener(mouseAdapter); + + MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); + myEditorComponent.addMouseMotionListener(mouseMotionListener); + myGutterComponent.addMouseMotionListener(mouseMotionListener); + + myEditorComponent.addFocusListener ( + new FocusAdapter() { + public void focusGained(FocusEvent e) { + myCaretCursor.activate(); + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusGained(); + } + public void focusLost(FocusEvent e) { + synchronized (ourCaretThread) { + if (ourCaretThread.myEditor == EditorImpl.this) { + ourCaretThread.myEditor = null; + } + } + int caretLine = getCaretModel().getLogicalPosition().line; + repaintLines(caretLine, caretLine); + fireFocusLost(); + } + } + ); + + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + + try { + myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() { + public void drop(DropTargetDropEvent dtde) { + } + + public void dragOver(DropTargetDragEvent dtde) { + Point location = dtde.getLocation(); + + moveCaretToScreenPos(location.x, location.y); + getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + }); + } catch (TooManyListenersException e) { + } + } + + private void processKeyTyped(char c) { + // [vova] This is patch for Mac OS X. Under Mac "input methods" + // is handled before our EventQueue consume upcoming KeyEvents. + IdeEventQueue queue = IdeEventQueue.getInstance(); + if(queue.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()){ + return; + } + ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); + DataContext dataContext = getDataContext(); + actionManager.fireBeforeEditorTyping(c, dataContext); + EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); + } + + private void fireFocusLost() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusLost(this); + } + } + + private FocusChangeListener[] getFocusListeners() { + return (FocusChangeListener[]) myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]); + } + + private void fireFocusGained() { + FocusChangeListener[] listeners = getFocusListeners(); + for (int i = 0; i < listeners.length; i++) { + FocusChangeListener listener = listeners[i]; + listener.focusGained(this); + } + } + + public void setHighlighter(Highlighter highlighter) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (myHighlighter != null) { + getDocument().removeDocumentListener(myHighlighter); + } + + getDocument().addDocumentListener(highlighter); + highlighter.setText(getDocument().getChars(), getDocument().getTextLength()); + myHighlighter = highlighter; + myHighlighter.setEditor(this); + reinitSettings(); + } + + public Highlighter getHighlighter() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myHighlighter; + } + + public JComponent getContentComponent() { + return myEditorComponent; + } + + public EditorGutterComponent getGutterComponent() { + return myGutterComponent; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + myPropertyChangeSupport.removePropertyChangeListener(listener); + } + + public void setInsertMode(boolean val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + boolean oldValue = isInsertMode; + isInsertMode = val; + myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, val); + //Repaint the caret line by moving caret to the same place + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + getCaretModel().moveToLogicalPosition(caretPosition); + } + + public void setBlockSelectionMode(boolean isBlockSelectionMode) { + myIsBlockSelectionMode = isBlockSelectionMode; + } + + public boolean isBlockSelectionMode() { + return myIsBlockSelectionMode; + } + + public boolean isInsertMode() { + return isInsertMode; + } + + private int yPositionToVisibleLineNumber(int y) { + return y/getLineHeight(); + } + + public static int getSpaceWidth(FontMetrics fontMetrics) { + int width = fontMetrics.charWidth(' '); + return width > 0 ? width : 1; + } + + public VisualPosition xyToVisualPosition(Point p) { + int line = yPositionToVisibleLineNumber(p.y); + + int x = 0; + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0))); + int textLength = myDocument.getTextLength(); + + if (offset >= textLength) return new VisualPosition(line, 0); + + int column = 0; + int prevX = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + char c = ' '; + IterationState state = new IterationState(this, offset, false); + + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(fontMetrics); + +outer: + while (true) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + c = placeholder[j]; + x += fontMetrics.charWidth(c); + if (x >= p.x) break outer; + column++; + } + offset = region.getEndOffset(); + } else { + prevX = x; + c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + x = nextTabStop(x); + } else { + x += fontMetrics.charWidth(c); + } + + if (x >= p.x) break; + + if (c == '\t') { + column += (x - prevX) / spaceSize; + } else { + column++; + } + + offset++; + } + } + + int charWidth = fontMetrics.charWidth(c); + + if (x >= p.x && c == '\t') { + if (mySettings.isCaretInsideTabs()) { + column += (p.x - prevX) / spaceSize; + if ((p.x - prevX) % spaceSize > spaceSize / 2) column++; + } else if ((x - p.x) *2 < x - prevX) { + column += (x - prevX) / spaceSize; + } + } else if (x >= p.x) { + if ((x - p.x) * 2 < charWidth) column++; + } else { + column += (p.x - x) / getSpaceWidth(fontMetrics); + } + + return new VisualPosition(line, column); + } + + public VisualPosition offsetToVisualPosition(int offset) { + return logicalToVisualPosition(offsetToLogicalPosition(offset)); + } + + public LogicalPosition offsetToLogicalPosition(int offset) { + int line = calcLogicalLineNumber(offset); + int column = calcColumnNumber(offset, line); + return new LogicalPosition(line, column); + } + + public LogicalPosition xyToLogicalPosition(Point p) { + final Point pp; + if (p.x >= 0 && p.y >= 0) { + pp = p; + } else { + pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0)); + } + + return visualToLogicalPosition(xyToVisualPosition(pp)); + } + + public Point logicalPositionToXY(LogicalPosition pos) { + VisualPosition visible = logicalToVisualPosition(pos); + int y = visibleLineNumberToYPosition(visible.line); + + int lineStartOffset; + + if (pos.line == 0) { + lineStartOffset = 0; + } else if (pos.line >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(pos.line); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + public Point visualPositionToXY(VisualPosition visible) { + int y = visibleLineNumberToYPosition(visible.line); + int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line; + + int lineStartOffset; + + if (logLine == 0) { + lineStartOffset = 0; + } else if (logLine >= myDocument.getLineCount()) { + lineStartOffset = myDocument.getTextLength(); + } else { + lineStartOffset = myDocument.getLineStartOffset(logLine); + } + + int x = getTabbedTextWidth(lineStartOffset, visible); + return new Point(x, y); + } + + private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) { + if (pos.column == 0) return 0; + + int x = 0; + int offset = lineStartOffset; + char[] text = myDocument.getCharsNoThreadCheck(); + int textLength = myDocument.getTextLength(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + int spaceSize = getSpaceWidth(getFontMetrics(myCurrentFontType)); + + int column = 0; +outer: + while (column < pos.column) { + if (offset >= textLength) break; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion region = state.getCurrentFold(); + + if (region != null) { + char placeholder[] = region.getPlaceholderText().toCharArray(); + for (int j = 0; j < placeholder.length; j++) { + x += fontMetrics.charWidth(placeholder[j]); + column++; + if (column >= pos.column) break outer; + } + offset = region.getEndOffset(); + } else { + int c = text[offset]; + if (c == '\n') { + break; + } else if (c == '\t') { + int prevX = x; + x = nextTabStop(x); + column += (x - prevX) / spaceSize; + } else { + x += fontMetrics.charWidth(c); + column++; + } + offset++; + } + } + + if (column != pos.column) { + x += getSpaceWidth(fontMetrics) * (pos.column - column); + } + + return x; + } + + public int nextTabStop(int x) { + int tabSize = mySettings.getTabSize(myProject); + if (tabSize <= 0) { + tabSize = 1; + } + + tabSize *= getSpaceWidth(getFontMetrics(Font.PLAIN)); + + int nTabs = x / tabSize; + return (nTabs + 1) * tabSize; + } + + public int visibleLineNumberToYPosition(int lineNum) { + if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum); + return lineNum*getLineHeight(); + } + + public void repaint(int startOffset, int endOffset) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myScrollPane == null) + return; + if(endOffset > myDocument.getTextLength()) { + endOffset = myDocument.getTextLength(); + } + if(startOffset < endOffset) { + int startLine = myDocument.getLineNumber(startOffset); + int endLine = myDocument.getLineNumber(endOffset); + repaintLines(startLine, endLine); + } + } + + public void repaintLines(int startLine, int endLine) { + Rectangle visibleRect = getScrollingModel().getVisibleArea(); + int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y; + int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT; + + myEditorComponent.repaintEditorComponent(visibleRect.x, + yStartLine, + visibleRect.x + visibleRect.width, + yEndLine - yStartLine); + myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); + } + + private void beforeChangedUpdate(DocumentEvent e) { + int oldStartVisualLine = offsetToVisualPosition(e.getOffset()).line; + int oldEndVisualLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line; + + if (e.getOldLength() > 0 && oldStartVisualLine <= myLongestLine && myLongestLine <= oldEndVisualLine) { + myLongestLine = -1; + myLongestLineLength = -1; + } else if (myLongestLine >= oldStartVisualLine) { + myLongestLine -= (oldEndVisualLine - oldStartVisualLine); + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); + myCaretUpdateVShift = pos.y - viewRect.y; + } + + private void changedUpdate(DocumentEvent e) { + if(myScrollPane == null) return; + + stopOptimizedScrolling(); + mySelectionModel.removeBlockSelection(); + + int startVisualLine = offsetToVisualPosition(e.getOffset()).line; + int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line; + + if (myLongestLine >= 0) { + if (startVisualLine <= myLongestLine) { + if (myLongestLine <= endVisualLine && e.getOldLength() > 0) { + myLongestLineLength = -1; + } else { + myLongestLine += (endVisualLine - startVisualLine); + } + } + + if (myDocument.getLineCount() == 0) { + recalcSize(); + } + else { + recalcSizeInRange(myDocument.getLineStartOffset(myDocument.getLineNumber(e.getOffset())), + myDocument.getLineEndOffset(myDocument.getLineNumber(e.getOffset() + e.getNewLength())), + myLongestLineLength, startVisualLine); + + } + } else { + recalcSize(); + } + + updateCaretCursor(); + + repaintLines(startVisualLine, endVisualLine); + + Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - myCaretUpdateVShift; + getScrollingModel().scrollVertically(scrollOffset); + } + + private void validateSize() { + if (myLongestLine < 0) recalcSize(); + + int documentWidth = getLineWidth(myLongestLine); + + int contentWidth = calcContentWidth(documentWidth); + int contentHeight = Math.max(getLineHeight(), calcContentHeight()); + + Rectangle viewRectangle = getScrollingModel().getVisibleArea(); + + if (myContentSize == null || contentWidth != myContentSize.width || contentHeight != myContentSize.height) { + int width = Math.max(contentWidth, viewRectangle.width); + int height = Math.max(contentHeight, viewRectangle.height); + mySize = new Dimension(width, height); + myContentSize = new Dimension(contentWidth, contentHeight); + + stopOptimizedScrolling(); + myEditorComponent.setSize(mySize); + myScrollPane.getVerticalScrollBar().setMaximum(mySize.height); + myScrollPane.getHorizontalScrollBar().setMaximum(mySize.width); + + int lineNum = Math.max(1, getDocument().getLineCount()); + myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth("" + (lineNum + 2)) + 6); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myEditorComponent.repaint(); + myScrollPane.repaint(); + myMarkupModel.repaint(); + } + } + + private int calcContentHeight() { + return getLineHeight() * (getVisibleLineCount() + mySettings.getAdditionalLinesCount()); + } + + private int calcContentWidth(int documentWidth) { + int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x; + return Math.max(caretX, documentWidth) + + mySettings.getAdditionalColumnsCount() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + void recalcSizeAndRepaint() { + recalcSize(); + validateSize(); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myGutterComponent.setSize(myGutterComponent.getPreferredSize()); + + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + + myMarkupModel.repaint(); + + stopOptimizedScrolling(); + myEditorComponent.repaintEditorComponent(); + + myGutterComponent.repaint(); + } + + public Document getDocument() { + return myDocument; + } + + public JComponent getComponent() { + return myPanel; + } + + public void addEditorMouseListener(EditorMouseListener listener) { + myMouseListeners.add(listener); + } + + public void removeEditorMouseListener(EditorMouseListener listener) { + boolean success = myMouseListeners.remove(listener); + LOG.assertTrue(success); + } + + public void addEditorMouseMotionListener(EditorMouseMotionListener listener) { + myMouseMotionListeners.add(listener); + } + + public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) { + boolean success = myMouseMotionListeners.remove(listener); + LOG.assertTrue(success); + } + + public void paint(Graphics g) { + myEditorComponent.setOpaque(true); + + flushDeferredSizeChanges(); + + Rectangle clip = getClipBounds(g); + + if(clip == null) { + return; + } + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(viewRect == null) { + return; + } + + if(isReleased) { + g.setColor(new Color(128, 255, 128)); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + return; + } + + Color background = myScheme.getColor(EditorColors.BACKGROUND_COLOR); + g.setColor(background == null ? Color.white : background); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + paintBackgrounds(g, clip); + paintRightMargin(g, clip); + paintLineMarkersSeparators(g, clip, myDocument.getMarkupModel(myProject)); + paintLineMarkersSeparators(g, clip, myMarkupModel); + paintText(g, clip); + paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip); + + paintCaretCursor(g); + + paintComposedTextDecoration((Graphics2D) g); + } + + private void paintComposedTextDecoration(Graphics2D g) { + if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) { + VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength())); + int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1; + Point p1 = visualPositionToXY(visStart); + Point p2 = logicalPositionToXY(offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, myDocument.getTextLength()))); + + Stroke saved = g.getStroke(); + BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2,0,2}, 0); + g.setStroke(dotted); + g.drawLine(p1.x, y, p2.x, y); + g.setStroke(saved); + } + } + + public void flushDeferredSizeChanges() { + if (myCursorUpdater != null) { + myCursorUpdater.run(); + } + } + + private Rectangle getClipBounds(Graphics g) { + return mySettings.isWrapAtRightMargin() ? new Rectangle(mySize) : g.getClipBounds(); + } + + private void paintRightMargin(Graphics g, Rectangle clip) { + Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); + if(!mySettings.isRightMarginShown() || rightMargin == null) { + return; + } + int x = mySettings.getRightMargin()*getSpaceWidth(getFontMetrics(Font.PLAIN)); + if(x >= clip.x && x < clip.x+clip.width) { + g.setColor(rightMargin); + g.drawLine(x, clip.y, x, clip.y+clip.height); + } + } + + private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) { + int startLineNumber = yPositionToVisibleLineNumber(clip.y); + int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1; + + RangeHighlighter[] segmentHighlighters; + segmentHighlighters = myDocument.getMarkupModel(myProject).getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighters[i], startLineNumber, + endLineNumber + ); + } + + segmentHighlighters = getMarkupModel().getAllHighlighters(); + for (int i = 0; i < segmentHighlighters.length; i++) { + RangeHighlighter segmentHighlighter = segmentHighlighters[i]; + paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx) segmentHighlighter, startLineNumber, endLineNumber); + } + } + + private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter, + int startLineNumber, int endLineNumber + ) { + if (!segmentHighlighter.isValid()) { + return; + } + if (segmentHighlighter.isAfterEndOfLine()) { + int startOffset = segmentHighlighter.getStartOffset(); + int visibleStartLine = offsetToVisualPosition(startOffset).line; + + if (!getFoldingModel().isOffsetCollapsed(startOffset)) { + if(visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) { + int logStartLine = offsetToLogicalPosition(startOffset).line; + LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); + Point end = logicalPositionToXY(logPosition); + int charWidth = getSpaceWidth(getFontMetrics(Font.PLAIN)); + int lineHeight = getLineHeight(); + TextAttributes attributes = segmentHighlighter.getTextAttributes(); + if(attributes != null && attributes.getBackgroundColor() != null) { + g.setColor(attributes.getBackgroundColor()); + g.fillRect(end.x, end.y, charWidth, lineHeight); + } + if(attributes != null && attributes.getEffectColor() != null) { + int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 1; + g.setColor(attributes.getEffectColor()); + if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { + drawWave(g, end.x, end.x + charWidth - 1, y); + } else { + g.drawLine(end.x, y, end.x + charWidth - 1, y); + } + } + } + } + } + } + + private int getLineWidth(int visualLine) { + if (visualLine < 0) return 0; + + if (visualLine == myLongestLine && myLongestLineLength >= 0) return myLongestLineLength; + + int x = 0; + + int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(visualLine, 0))); + return getLineTailWidth(offset, x, visualLine); + } + + private int getLineTailWidth(int offset, int x, int visualLine) { + int end = myDocument.getTextLength(); + + char[] text = myDocument.getCharsNoThreadCheck(); + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + break; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + + if (myLongestLine == visualLine) myLongestLineLength = x; + + return x; + } + + public int getMaxWidthInRange(int startOffset, int endOffset) { + int width = 0; + VisualPosition start = offsetToVisualPosition(startOffset); + VisualPosition end = offsetToVisualPosition(endOffset); + + for (int i = start.line; i <= end.line; i++) { + int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; + int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; + + if (lineWidth > width) { + width = lineWidth; + } + } + + return width; + } + + private void paintBackgrounds(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color backColor = attributes.getBackgroundColor(); + Point position = new Point(0, visibleLineNumber * lineHeight); + int fontType = attributes.getFontType(); + char[] text = myDocument.getCharsNoThreadCheck(); + int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); + while (!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if (hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + + if (lIterator.getLineNumber() < lastLineIndex) { + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } else { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + break; + } + + position.x = 0; + if (position.y > clip.y + clip.height) break; + position.y += lineHeight; + start = lEnd; + } + + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText().toCharArray(), 0, + collapsedFolderAt.getPlaceholderText().length(), position, fontType + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength() - start, + position, fontType + ); + } else { + position.x = drawBackground(g, backColor, text, start, hEnd - start, position, fontType); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + backColor = attributes.getBackgroundColor(); + fontType = attributes.getFontType(); + start = iterationState.getStartOffset(); + } + } + + if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { + paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight); + } + } + + private static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position, + Rectangle clip, int lineHeight + ) { + Color backColor = iterationState.getPastFileEndBackground(); + if (backColor != null) { + Color saved = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); + g.setColor(saved); + } + } + + private int drawBackground(Graphics g, Color backColor, char[] text, int offset, int length, Point position, int fontType) { + int w = getTextSegmentWidth(text, offset, length, position.x, fontType); + + if(backColor != null) { + Color savedColor = g.getColor(); + g.setColor(backColor); + g.fillRect(position.x, position.y, w, getLineHeight()); + g.setColor(savedColor); + } + + return position.x + w; + } + + private class LineIteratorWrapper implements LineIterator { + private int myRightMargin; + private int myCurrentWidth; + private int myEnd; + private int myStart; + + public LineIteratorWrapper() { + myRightMargin = mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN)); + } + + public void start(int startOffset) { + myStart = myDocument.getLineStartOffset(myDocument.getLineNumber(startOffset)); + myEnd = myStart; + myCurrentWidth = 0; + advanceOffset(); + } + + private void advanceOffset() { + if (atEnd()) return; + + char[] text = myDocument.getCharsNoThreadCheck(); + myStart = myEnd; + +outer: + while (myEnd < myDocument.getTextLength()) { + char c = text[myEnd]; + + if (c == '\t') { + myCurrentWidth = nextTabStop(myCurrentWidth); + } else if (c == '\n') { + myEnd++; + break outer; + } else { + myCurrentWidth += getFontMetrics(Font.PLAIN).charWidth(c); + } + + myEnd++; + + if (myCurrentWidth > myRightMargin) { + int savedEnd = myEnd; + while (myEnd > myStart) { + myEnd--; + if (Character.isSpaceChar(text[myEnd]) || Character.isWhitespace(text[myEnd])) break outer; + } + myEnd = savedEnd; + + while (myCurrentWidth > myRightMargin) { + myEnd--; + myCurrentWidth -= getFontMetrics(Font.PLAIN).charWidth(text[myEnd]); + } + } + } + + myCurrentWidth = 0; + } + + public int getStart() { + return myStart; + } + + public int getEnd() { + return Math.min(myEnd, myDocument.getTextLength()); + } + + public int getSeparatorLength() { + if (myEnd == 0) return 0; + return myDocument.getCharsNoThreadCheck()[myEnd - 1] == '\n' ? 1 : 0; + } + + public int getLineNumber() { + return 0; + } + + public void advance() { + advanceOffset(); + } + + public boolean atEnd() { + return myEnd >= myDocument.getTextLength(); + } + } + + private LineIterator createLineIterator() { + if (mySettings.isWrapAtRightMargin()) { + return new LineIteratorWrapper(); + } + + return myDocument.createLineIterator(); + } + + private void paintText(Graphics g, Rectangle clip) { + int lineHeight = getLineHeight(); + + int visibleLineNumber = clip.y/lineHeight; + + int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line; + + if(startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) + return; + + int start = myDocument.getLineStartOffset(startLineNumber); + + IterationState iterationState = new IterationState(this, start, true); + + LineIterator lIterator = createLineIterator(); + lIterator.start(start); + if(lIterator.atEnd()) + return; + + TextAttributes attributes = iterationState.getMergedAttributes(); + Color currentColor = attributes.getForegroundColor(); + if(currentColor == null) { + currentColor = Color.black; + } + Color effectColor = attributes.getEffectColor(); + EffectType effectType = attributes.getEffectType(); + int fontType = attributes.getFontType(); + myCurrentFontType = fontType; + Font currentFont = getFont(fontType); + g.setColor(currentColor); + g.setFont(currentFont); + Point position = new Point(0, visibleLineNumber * lineHeight); + while(!iterationState.atEnd() && !lIterator.atEnd()) { + int hEnd = iterationState.getEndOffset(); + int lEnd = lIterator.getEnd(); + + if(hEnd >= lEnd) { + FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); + if (collapsedFolderAt == null) { + drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, + fontType, currentColor + ); + position.x = 0; + position.y += lineHeight; + if (position.y > clip.y + clip.height + lineHeight) break; + start = lEnd; + } + + if (myBorderColor != null) flushBorder(g); + lIterator.advance(); + } else { + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, + fontType, currentColor + ); + } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { + position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, + effectType, fontType, currentColor + ); + } else { + position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor); + } + + iterationState.advance(); + attributes = iterationState.getMergedAttributes(); + Color color = attributes.getForegroundColor(); + if(color == null) { + color = Color.black; + } + if(color != currentColor) { + g.setColor(color); + currentColor = color; + } + effectColor = attributes.getEffectColor(); + effectType = attributes.getEffectType(); + fontType = attributes.getFontType(); + + start = iterationState.getStartOffset(); + } + } + + FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); + if (collapsedFolderAt != null) { + drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor); + flushBorder(g); + } + + flushCachedChars(g); + } + + private class CachedFontContent { + final char[][] data = new char[CACHED_CHARS_BUFFER_SIZE][]; + final int[] start = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] length = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; + final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; + final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; + + int myCount = 0; + final int myFontType; + + public CachedFontContent(int fontType) { + myFontType = fontType; + } + + public void flushContent(Graphics g) { + if (myCount != 0) { + if (myCurrentFontType != myFontType) { + myCurrentFontType = myFontType; + g.setFont(getFont(myFontType)); + } + + Color currentColor = null; + for (int i = 0; i < myCount; i++) { + if (!color[i].equals(currentColor)) { + currentColor = color[i]; + g.setColor(currentColor); + } + + g.drawChars(data[i], start[i], length[i], x[i], y[i]); + color[i] = null; + data[i] = null; + } + + myCount = 0; + } + } + + public void addContent(Graphics g, char[] data, int start, int length, int x, int y, Color color) { + this.data[myCount] = data; + this.start[myCount] = start; + this.length[myCount] = length; + this.x[myCount] = x; + this.y[myCount] = y; + this.color[myCount] = color; + + myCount++; + if (myCount >= CACHED_CHARS_BUFFER_SIZE) { + flushContent(g); + } + } + } + + private void flushCachedChars(Graphics g) { + myPlainCache.flushContent(g); + myBoldCache.flushContent(g); + myBoldItalicCache.flushContent(g); + myItalicCache.flushContent(g); + } + + private void paintCaretCursor(Graphics g) { + myCaretCursor.paint(g); + } + + private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) { + RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters(); + for (int i = 0; i < lineMarkers.length; i++) { + RangeHighlighter lineMarker = lineMarkers[i]; + paintLineMarkerSeparator(lineMarker, clip, g); + } + } + + private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) { + if (!marker.isValid()) { + return; + } + Color separatorColor = marker.getLineSeparatorColor(); + if (separatorColor != null) { + int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() + .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); + if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) { + return; + } + + int endShift = clip.x + clip.width; + g.setColor(separatorColor); + + if (mySettings.isRightMarginShown() + && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { + endShift = Math.min(endShift, mySettings.getRightMargin() * getSpaceWidth(getFontMetrics(Font.PLAIN))); + } + + int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line); + + if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) { + y += getLineHeight(); + } + + g.drawLine(0, y - 1, endShift, y - 1); + } + } + + private Font getFont(int type) { + if(type == Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD); + else if(type == Font.ITALIC) + return myScheme.getFont(EditorFontType.ITALIC); + else if(type == Font.ITALIC + Font.BOLD) + return myScheme.getFont(EditorFontType.BOLD_ITALIC); + else + return myScheme.getFont(EditorFontType.PLAIN); + } + + private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + if (start >= end) return position.x; + + char[] text = myDocument.getCharsNoThreadCheck(); + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text, start, end - start, x, y, effectColor, effectType, fontType, fontColor); + } + + private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height); + + if (!isInClip) return position.x; + + int y = getLineHeight() - getDescent() + position.y; + int x = position.x; + return drawTabbedString(g, text.toCharArray(), 0, text.length(), x, y, effectColor, effectType, fontType, + fontColor + ); + } + + private int drawTabbedString(Graphics g, char[] text, int offset, int length, int x, int y, Color effectColor, + EffectType effectType, int fontType, Color fontColor + ) { + int xStart = x; + + int start = offset; + + for (int i = offset; i < offset + length; i++) { + if (text[i] != '\t') continue; + + if (i > start) { + drawCharsCached(g, text, start, i - start, x, y, fontType, fontColor); + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + drawCharsCached(g, text, start, offset + length - start, x, y, fontType, fontColor); + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + + if(effectColor != null) { + Color savedColor = g.getColor(); + + int w = getTextSegmentWidth(text, offset, length, xStart, fontType); + if (effectType == EffectType.LINE_UNDERSCORE) { + g.setColor(effectColor); + g.drawLine(xStart, y+1, xStart+w, y+1); + g.setColor(savedColor); + } else if (effectType == EffectType.STRIKEOUT) { + g.setColor(effectColor); + int y1 = y-getCharHeight() / 2; + g.drawLine(xStart, y1, xStart+w, y1); + g.setColor(savedColor); + } else if (effectType == EffectType.WAVE_UNDERSCORE) { + g.setColor(effectColor); + drawWave(g, xStart, xStart + w, y + 1); + g.setColor(savedColor); + } else if (effectType == EffectType.BOXED) { + if (myBorderStart == -1) { + if (myBorderColor != effectColor) { + flushBorder(g); + myBorderStart = xStart; + } + myBorderY = y - getLineHeight() + getDescent(); + myBorderColor = effectColor; + } + myBorderEnd = xStart + w; + } + } else { + flushBorder(g); + } + + return x; + } + + private void drawCharsCached(Graphics g, char[] data, int start, int length, int x, int y, int fontType, Color color) { + if (fontType == myCurrentFontType) { + g.drawChars(data, start, length, x, y); + } else if (fontType == Font.PLAIN) { + myPlainCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.BOLD) { + myBoldCache.addContent(g, data, start, length, x, y, color); + } else if (fontType == Font.ITALIC) { + myItalicCache.addContent(g, data, start, length, x, y, color); + } else { + myBoldItalicCache.addContent(g, data, start, length, x, y, color); + } + } + + private void flushBorder(Graphics g) { + if (myBorderStart != -1) { + Color savedColor = g.getColor(); + g.setColor(myBorderColor); + g.drawRect(myBorderStart, myBorderY, myBorderEnd - myBorderStart - 1, getLineHeight() - 1); + g.setColor(savedColor); + myBorderStart = -1; + myBorderEnd = -1; + myBorderY = 0; + myBorderColor = null; + } + } + + private static final int WAVE_HEIGHT = 2; + private static final int WAVE_SEGMENT_LENGTH = 4; + + private void drawWave(Graphics g, int xStart, int xEnd, int y) { + int startSegment = xStart / WAVE_SEGMENT_LENGTH; + int endSegment = xEnd / WAVE_SEGMENT_LENGTH; + for (int i = startSegment; i < endSegment; i++) { + drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); + } + + int x = WAVE_SEGMENT_LENGTH * endSegment; + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + } + + private static void drawWaveSegment(Graphics g, int x, int y) { + g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); + g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); + } + + private int getTextSegmentWidth(char[]text, int offset, int length, int xStart, int fontType) { + int start = offset; + int x = xStart; + + for(int i=offset; i start) { + for (int j = start; j < i; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + x = nextTabStop(x); + start = i+1; + } + + if(offset+length > start) { + for (int j = start; j < offset + length; j++) x += getFontMetrics(fontType).charWidth(text[j]); + } + return x - xStart; + } + + public int getLineHeight() { + if(myLineHeight != -1) return myLineHeight; + + ApplicationManager.getApplication().assertIsDispatchThread(); + + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myLineHeight = (int)(fontMetrics.getHeight()*myScheme.getLineSpacing()); + if(myLineHeight == 0) { + myLineHeight = fontMetrics.getHeight(); + if(myLineHeight == 0) { + myLineHeight = 12; + } + } + +/* + recalcSize(0, myDocument.getTextLength()); + + Dimension size = getPreferredSize(); + myEditorComponent.setSize(size); + myScrollPane.getVerticalScrollBar().setMaximum(size.height); + myScrollPane.getHorizontalScrollBar().setMaximum(size.width); + myScrollPane.revalidate(); + // myErrorPanel.revalidate(); + myEditorComponent.repaintEditorComponent(); + myGutterComponent.repaint(); +*/ + + return myLineHeight; + } + + int getDescent() { + if (myDescent != -1) + return myDescent; + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myDescent = fontMetrics.getDescent(); + return myDescent; + } + + public FontMetrics getFontMetrics(int fontType) { + if (myPlainFontMetrics == null) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); + myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); + myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); + } + + if (fontType == Font.PLAIN) return myPlainFontMetrics; + if (fontType == Font.BOLD) return myBoldFontMetrics; + if (fontType == Font.ITALIC) return myItalicFontMetrics; + if (fontType == (Font.BOLD + Font.ITALIC)) return myBoldItalicFontMetrics; + + LOG.assertTrue(false); + + return null; + } + + private int getCharHeight() { + if(myCharHeight == -1) { + ApplicationManager.getApplication().assertIsDispatchThread(); + FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); + myCharHeight = fontMetrics.charWidth('a'); + } + return myCharHeight; + } + + public Dimension getPreferredSize() { + if(mySize == null) validateSize(); + return mySize; + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private void recalcSize() { + recalcSizeInRange(0, getDocument().getTextLength(), 0, 0); + + myContentSize = new Dimension(Math.max(0, myLongestLineLength), myLongestLine * getLineHeight()); + } + + private void recalcSizeInRange(int offset, int end, int curMaxLineWidth, int startLine) { + int x = 0; + char[] text = myDocument.getCharsNoThreadCheck(); + + IterationState state = new IterationState(this, offset, false); + FontMetrics fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + + while (offset < end) { + char c = text[offset]; + if (x > curMaxLineWidth) { + curMaxLineWidth = x; + myLongestLine = startLine; + myLongestLineLength = x; + } + + if (offset >= state.getEndOffset()) { + state.advance(); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } + + FoldRegion collapsed = state.getCurrentFold(); + if (collapsed != null) { + String placeholder = collapsed.getPlaceholderText(); + for (int i = 0; i < placeholder.length(); i++) { + x += fontMetrics.charWidth(placeholder.charAt(i)); + } + offset = collapsed.getEndOffset(); + } else if (c == '\t') { + x = nextTabStop(x); + offset++; + } else if (c == '\n') { + x = 0; + offset++; + startLine++; + } else { + x += fontMetrics.charWidth(c); + offset++; + } + } + } + + public int logicalPositionToOffset(LogicalPosition pos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if(myDocument.getLineCount() == 0) return 0; + + if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); + if(pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); + + if(pos.line >= myDocument.getLineCount()) { + return myDocument.getTextLength(); + } + + int start = myDocument.getLineStartOffset(pos.line); + int end = myDocument.getLineEndOffset(pos.line); + + char[] text = myDocument.getCharsNoThreadCheck(); + + if (pos.column == 0) return start; + return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject)); + } + + public void setLastColumnNumber(int val) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myLastColumnNumber = val; + } + + public int getLastColumnNumber() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myLastColumnNumber; + } + + int getVisibleLineCount() { + int line = getDocument().getLineCount(); + line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); + return line; + } + + public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new VisualPosition(logicalPos.line, logicalPos.column); + + int offset = logicalPositionToOffset(logicalPos); + + FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); + if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { + if (offset < getDocument().getTextLength() - 1) { + offset = outermostCollapsed.getStartOffset(); + LogicalPosition foldStart = offsetToLogicalPosition(offset); + return logicalToVisualPosition(foldStart); + } else { + offset = outermostCollapsed.getEndOffset() + 3; + } + } + + int line = logicalPos.line; + int column = logicalPos.column; + + line -= myFoldingModel.getFoldedLinesCountBefore(offset); + FoldRegion lastBefore = myFoldingModel.getLastCollapsedBefore(offset); + + if (lastBefore != null && lastBefore.getDocument().getLineNumber(lastBefore.getEndOffset()) == logicalPos.line && + lastBefore.getEndOffset() - 1 < offset) { + LogicalPosition foldStart = offsetToLogicalPosition(lastBefore.getStartOffset()); + LogicalPosition foldEnd = offsetToLogicalPosition(lastBefore.getEndOffset() - 1); + column += foldStart.column + lastBefore.getPlaceholderText().length() - foldEnd.column - 1; + } + + + LOG.assertTrue(line >= 0); + + return new VisualPosition(line, Math.max(0, column)); + } + + private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) { + FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); + + if (topLevelCollapsed == null) return null; + + int start = 0; + int end = topLevelCollapsed.length - 1; + int i = 0; + + while (start <= end) { + i = (start + end) / 2; + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line < visual.line) { + start = i + 1; + } else if (visFoldEnd.line > visual.line) { + end = i - 1; + } else if (visFoldEnd.column < visual.column) { + start = i + 1; + } else if (visFoldEnd.column > visual.column) { + end = i - 1; + } else { + i--; + break; + } + } + + while (i >= 0 && i < topLevelCollapsed.length) { + if (topLevelCollapsed[i].isValid()) break; + i--; + } + + if (i >= 0 && i < topLevelCollapsed.length) { + FoldRegion region = topLevelCollapsed[i]; + LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) { + i--; + if (i >= 0) { + return topLevelCollapsed[i]; + } else { + return null; + } + } + return region; + } + + return null; + } + + public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) { + ApplicationManager.getApplication().assertIsDispatchThread(); + if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); + + int line = visiblePos.line; + int column = visiblePos.column; + + FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); + + if (lastCollapsedBefore != null) { + LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset() - 1); + VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd); + + line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line); + if (visFoldEnd.line == visiblePos.line && visiblePos.column > visFoldEnd.column) { + LogicalPosition foldStart = offsetToLogicalPosition(lastCollapsedBefore.getStartOffset()); + column -= foldStart.column + lastCollapsedBefore.getPlaceholderText().length() - logFoldEnd.column - 1; + } + } + + if (column < 0) column = 0; + + int offset = logicalPositionToOffset(new LogicalPosition(line, column)); + FoldRegion collapsedAt = myFoldingModel.fetchOutermost(offset); + if (collapsedAt != null) { + return offsetToLogicalPosition(collapsedAt.getStartOffset()); + } + + return new LogicalPosition(line, column); + } + + private int calcLogicalLineNumber(int offset) { + int textLength = myDocument.getTextLength(); + if(textLength == 0) return 0; + + if (offset > textLength || offset < 0) throw new IndexOutOfBoundsException( + "Wrong offset: " + offset + " textLength: " + textLength + ); + + int lineIndex = myDocument.getLineNumber(offset); + + LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); + + return lineIndex; + } + + private int calcColumnNumber(int offset, int lineIndex) { + if(myDocument.getTextLength() == 0) return 0; + + char[] text = myDocument.getChars(); + int start = myDocument.getLineStartOffset(lineIndex); + if (start == offset) return 0; + return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject)); + } + + private void moveCaretToScreenPos(int x, int y) { + if(x < 0) { + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); + + int columnNumber = pos.column; + int lineNumber = pos.line; + + if(lineNumber >= myDocument.getLineCount()) { + lineNumber = myDocument.getLineCount() - 1; + } + if(!mySettings.isVirtualSpace()) { + if(lineNumber >= 0) { + int lineEndOffset = myDocument.getLineEndOffset(lineNumber); + int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber); + if(columnNumber > lineEndColumnNumber) { + columnNumber = lineEndColumnNumber; + } + } + } + if(lineNumber < 0) { + lineNumber = 0; + columnNumber = 0; + } + if(!mySettings.isCaretInsideTabs()) { + int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber)); + char[] text = myDocument.getChars(); + if(offset >= 0 && offset < myDocument.getTextLength()) { + if(text[offset] == '\t') { + columnNumber = calcColumnNumber(offset, lineNumber); + } + } + } + LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos1); + } + + private void runMousePressedCommand(final MouseEvent e) { + myMousePressedEvent = e; + EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e)); + + EditorMouseListener[] mouseListeners = (EditorMouseListener[])myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]); + for(int i=0; i 1000) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + } + + int x = e.getX(); + int y = e.getY(); + + if (x < 0) x = 0; + if (y < 0) y = 0; + + if (getMouseEventArea(e) == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { + final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); + if (range != null) { + final boolean expansion = !range.isExpanded(); + + int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); + Runnable processor = new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + range.setExpanded(expansion); + } + }; + getFoldingModel().runBatchFoldingOperation(processor); + y = myGutterComponent.getHeadCenterY(range); + getScrollingModel().scrollVertically(y - scrollShift); + return; + } + } + + if (e.getSource() == myGutterComponent) { + if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mousePressed(e); + if (e.isConsumed()) return; + } + x = 0; + } + + LogicalPosition pos = xyToLogicalPosition(new Point(e.getX(), e.getY())); + int columnNumber = pos.column; + int lineNumber = pos.line; + + LogicalPosition oldCaret = getCaretModel().getLogicalPosition(); + int oldCaretOffset = getCaretModel().getOffset(); + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + moveCaretToScreenPos(x, y); + + if (e.isPopupTrigger()) return; + + int caretOffset = getCaretModel().getOffset(); + + myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); + myMousePressedInsideSelection = mySelectionModel.hasSelection() && + caretOffset >= mySelectionModel.getSelectionStart() && + caretOffset <= mySelectionModel.getSelectionEnd() && + caretOffset != oldCaretOffset; + + if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { + int[] starts = mySelectionModel.getBlockSelectionStarts(); + int[] ends = mySelectionModel.getBlockSelectionEnds(); + for (int i = 0; i < starts.length; i++) { + if (caretOffset >= starts[i] && caretOffset < ends[i]) { + myMousePressedInsideSelection = true; + break; + } + } + } + + if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretOffset < mySavedSelectionStart) { + mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); + } else { + mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); + } + } else { + mySelectionModel.setSelection(oldSelectionStart, caretOffset); + } + } else if (columnNumber != oldCaret.column || lineNumber != oldCaret.line) { + setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); + if (!myMousePressedInsideSelection) { + mySelectionModel.setSelection(caretOffset, caretOffset); + } + } else if (!e.isPopupTrigger()) { + switch(e.getClickCount()) { + case 2: + mySelectionModel.selectWordAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + + case 3: + + mySelectionModel.selectLineAtCaret(); + setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); + mySavedSelectionStart = mySelectionModel.getSelectionStart(); + mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); + break; + } + } + + requestFocus(); + } + + private static boolean isControlKeyDown(MouseEvent mouseEvent) { + return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown(); + } + + private void processMouseReleased(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + myGutterComponent.mouseReleased(e); + } + + if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { + return; + } + + if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); + final FoldRegion region = ((FoldingModelEx) getFoldingModel()).getFoldingPlaceholderAt(e.getPoint()); + if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { + getFoldingModel().runBatchFoldingOperation(new Runnable() { + public void run() { + myFoldingModel.flushCaretShift(); + region.setExpanded(true); + } + }); + } + + } + + DataContext getDataContext() { + return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); + } + + public DataContext getProjectAwareDataContext(final DataContext original) { + if (original.getData(DataConstants.PROJECT) == myProject) return original; + + return new DataContext() { + public Object getData(String dataId) { + if (DataConstants.PROJECT.equals(dataId)) { + return myProject; + } + return original.getData(dataId); + } + }; + } + + + private EditorMouseEventArea getMouseEventArea(MouseEvent e) { + if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; + + int x = e.getX(); + + if (x >= myGutterComponent.getLineNumberAreaOffset() && + x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) + return EditorMouseEventArea.LINE_NUMBERS_AREA; + + if (x >= myGutterComponent.getLineMarkerAreaOffset() && + x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) + return EditorMouseEventArea.LINE_MARKERS_AREA; + + if (x >= myGutterComponent.getFoldingAreaOffset() && + x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) + return EditorMouseEventArea.FOLDING_OUTLINE_AREA; + + return null; + } + + private void requestFocus() { + myEditorComponent.requestFocus(); + } + + private void validateMousePointer(MouseEvent e) { + if (e.getSource() == myGutterComponent) { + FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); + myGutterComponent.setActiveFoldRegion(foldingAtCursor); + if (foldingAtCursor != null) { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } else { + myGutterComponent.setActiveFoldRegion(null); + if (getSelectionModel().hasSelection() && + (e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == 0) { + int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); + if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + return; + } + } + myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + } + } + + private void runMouseDraggedCommand(final MouseEvent e) { + if(myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { + return; + } + myCommandProcessor.executeCommand(myProject, new Runnable() { + public void run() { + processMouseDragged(e); + } + }, "", MOUSE_DRAGGED_GROUP); + } + + private void processMouseDragged(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + return; + } + Rectangle rect = getScrollingModel().getVisibleArea(); + + int dx = 0; + int x = e.getX(); + + if (e.getSource() == myGutterComponent) { + x = 0; + } + + if (x < rect.x) { + dx = x - rect.x; + } else if (x > rect.x + rect.width) { + dx = x - rect.x - rect.width; + } + + int dy = 0; + int y = e.getY(); + if (y < rect.y) { + dy = y - rect.y; + } else if (y > rect.y + rect.height) { + dy = y - rect.y - rect.height; + } + if (dx == 0 && dy == 0) { + myScrollingTimer.stop(); + + SelectionModelEx selectionModel = (SelectionModelEx) getSelectionModel(); + int oldSelectionStart = selectionModel.getLeadSelectionOffset(); + int oldCaretOffset = getCaretModel().getOffset(); + LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); + moveCaretToScreenPos(x, y); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (myMousePressedEvent != null && + getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA) { + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else { + if (e.isAltDown()) { + final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; + selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition()); + } else if (!myMousePressedInsideSelection) { + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + selectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + selectionModel.setSelection(oldSelectionStart, newCaretOffset); + } else if (caretShift != 0) { + if (myMousePressedEvent != null) { + boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); + mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; + getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e, + isCopy ? TransferHandler.COPY : TransferHandler.MOVE + ); + myMousePressedEvent = null; + } + } + } + } else { + myScrollingTimer.start(dx, dy); + } + } + + private static class RepaintCursorThread extends Thread { + private long mySleepTime = 500; + private boolean myIsBlinkCaret = true; + private EditorImpl myEditor = null; + private boolean isStopped = false; + private MyRepaintRunnable myRepaintRunnable; + + public RepaintCursorThread() { + super("EditorCaretThread"); + myRepaintRunnable = new MyRepaintRunnable(); + } + + private class MyRepaintRunnable implements Runnable { + public void run() { + if (myEditor != null) { + myEditor.myCaretCursor.repaint(); + } + } + } + + public void setBlinkPeriod(int blinkPeriod) { + mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; + } + + public void setBlinkCaret(boolean value) { + myIsBlinkCaret = value; + } + + public synchronized void stopThread() { + isStopped = true; + } + + public void run() { + while(true) { + try { + Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000); + } + catch(InterruptedException e) { + } + + synchronized(this) { + if(isStopped) { + break; + } + } + + if(myEditor == null) { + continue; + } + CaretCursor activeCursor = myEditor.myCaretCursor; + + long time = System.currentTimeMillis(); + time -= activeCursor.myStartTime; + + if(time > mySleepTime) { + boolean toRepaint = true; + if(myIsBlinkCaret) { + activeCursor.isVisible = !activeCursor.isVisible; + } + else { + + toRepaint = !activeCursor.isVisible; + activeCursor.isVisible = true; + } + + if(toRepaint) { + SwingUtilities.invokeLater(myRepaintRunnable); + } + } + } + } + } + + void updateCaretCursor() { + if (!IJSwingUtilities.hasFocus(getContentComponent())) { + stopOptimizedScrolling(); + } + + if (myCursorUpdater == null) { + myCursorUpdater = new Runnable() { + public void run() { + myCursorUpdater = null; + validateSize(); + char[] text = myDocument.getCharsNoThreadCheck(); + int offset = getCaretModel().getOffset(); + char c = offset >= myDocument.getTextLength() ? ' ' : text[offset]; + if (c == '\t' || c == '\n') c = ' '; + + FontMetrics fontMetrics; + + if (offset < myDocument.getTextLength()) { + IterationState state = new IterationState(EditorImpl.this, offset, false); + fontMetrics = getFontMetrics(state.getMergedAttributes().getFontType()); + } else { + fontMetrics = getFontMetrics(Font.PLAIN); + } + + int width = fontMetrics.charWidth(c); + myCaretCursor.setPosition(visualPositionToXY(getCaretModel().getVisualPosition()), width); + } + }; + + SwingUtilities.invokeLater(myCursorUpdater); + } + } + + public void setCaretVisible(boolean b) { + if (b) { + myCaretCursor.activate(); + } else { + myCaretCursor.passivate(); + } + } + + public void addFocusListener(FocusChangeListener listener) { + myFocusListeners.add(listener); + } + + public Project getProject() { + return myProject; + } + + public boolean isOneLineMode() { + return myIsOneLineMode; + } + + public void setOneLineMode(boolean isOneLineMode) { + myIsOneLineMode = isOneLineMode; + } + + public void stopOptimizedScrolling() { + myEditorComponent.setOpaque(false); + } + + private class CaretCursor { + private Point myLocation; + private int myWidth; + private boolean isVisible = true; + private long myStartTime = 0; + + public CaretCursor() { + myLocation = new Point(0, 0); + } + + public void activate() { + synchronized (ourCaretThread) { + ourCaretThread.myEditor = EditorImpl.this; + ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret()); + ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); + isVisible = true; + } + } + + public void passivate() { + synchronized(ourCaretThread) { + isVisible = false; + } + } + + public void setPosition(Point location, int width) { + myStartTime = System.currentTimeMillis(); + myLocation = location; + isVisible = true; + myWidth = Math.max(width, 2); + repaint(); + } + + private void repaint() { + EditorImpl.this.myEditorComponent.repaintEditorComponent( + myLocation.x, + myLocation.y, + myWidth, + getLineHeight() + ); + } + + public void paint(Graphics g) { + if(!isVisible || !IJSwingUtilities.hasFocus(getContentComponent())) return; + + int x = myLocation.x; + int lineHeight = getLineHeight(); + int y = myLocation.y; + + Rectangle viewRect = getScrollingModel().getVisibleArea(); + if(x - viewRect.x < 0) { + return; + } + + + g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); + if(!SystemInfo.isMac) { + Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR); + g.setXORMode(background != null ? background : Color.white); + } + + if (EditorImpl.this.isInsertMode != mySettings.isBlockCursor()){ + for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { + g.drawLine(x + i, y, x + i, y + lineHeight - 1); + } + } + else{ + g.fillRect(x, y, myWidth, lineHeight - 1); + } + + g.setPaintMode(); + } + } + + private class ScrollingTimer { + Timer myTimer; + private static final int TIMER_PERIOD = 100; + private static final int CYCLE_SIZE = 20; + private int myXCycles ; + private int myYCycles; + private int myDx; + private int myDy; + private int xPassedCycles = 0; + private int yPassedCycles = 0; + public void start(int dx, int dy) { + myDx = 0; + myDy = 0; + if(dx > 0) { + myXCycles = CYCLE_SIZE/dx+1; + myDx = 1+dx/CYCLE_SIZE; + } + else if(dx < 0) { + myXCycles = -CYCLE_SIZE/dx+1; + myDx = -1+dx/CYCLE_SIZE; + } + + if(dy > 0) { + myYCycles = CYCLE_SIZE/dy+1; + myDy = 1+dy/CYCLE_SIZE; + } + else if(dy < 0) { + myYCycles = -CYCLE_SIZE/dy+1; + myDy = -1+dy/CYCLE_SIZE; + } + + if(myTimer != null) { + return; + } + myTimer = new Timer(TIMER_PERIOD, + new ActionListener() { + public void actionPerformed(ActionEvent e) { + myCommandProcessor.executeCommand( + myProject, new Runnable() { + public void run() { + int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); + LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); + int columnNumber = caretPosition.column; + xPassedCycles++; + if(xPassedCycles >= myXCycles) { + xPassedCycles = 0; + columnNumber += myDx; + } + + int lineNumber = caretPosition.line; + yPassedCycles++; + if(yPassedCycles >= myYCycles) { + yPassedCycles = 0; + lineNumber += myDy; + } + + LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber); + getCaretModel().moveToLogicalPosition(pos); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + + int newCaretOffset = getCaretModel().getOffset(); + int caretShift = newCaretOffset - mySavedSelectionStart; + + if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { + if (caretShift < 0) { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretStart(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0)) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionEnd, newSelection); + } else { + int newSelection = newCaretOffset; + if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { + newSelection = mySelectionModel.getWordAtCaretEnd(); + } else if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { + newSelection = logicalPositionToOffset( + visualToLogicalPosition( + new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0) + ) + ); + } + if (newSelection < 0) newSelection = newCaretOffset; + mySelectionModel.setSelection(mySavedSelectionStart, newSelection); + } + return; + } + + if (mySelectionModel.hasBlockSelection()) { + mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); + } + else { + mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); + } + } + }, + "Move Cursor", null + ); + } + } + ); + myTimer.start(); + } + + public void stop() { + if(myTimer != null) { + myTimer.stop(); + myTimer = null; + } + } + + } + + class MyScrollBar extends JScrollBar { + + public MyScrollBar(int orientation) { + super(orientation); + } + + /** + * This is helper method. It returns height of the top (descrease) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getDecScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field decrButtonField=BasicScrollBarUI.class.getDeclaredField("decrButton"); + decrButtonField.setAccessible(true); + JButton decrButtonValue=(JButton)decrButtonField.get(ui); + LOG.assertTrue(decrButtonValue!=null); + return insets.top + decrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.top+15; + } + } + + /** + * This is helper method. It returns height of the bottom (increase) scrollbar + * button. Please note, that it's possible to return real height only if scrollbar + * is instance of BasicScrollBarUI. Otherwise it return fake (but good enough :) ) + * value. + */ + int getIncScrollButtonHeight(){ + ScrollBarUI ui=getUI(); + Insets insets = getInsets(); + if(ui instanceof BasicScrollBarUI){ + try{ + Field incrButtonField=BasicScrollBarUI.class.getDeclaredField("incrButton"); + incrButtonField.setAccessible(true); + JButton incrButtonValue=(JButton)incrButtonField.get(ui); + LOG.assertTrue(incrButtonValue!=null); + return insets.bottom + incrButtonValue.getHeight(); + }catch(Exception exc){ + exc.printStackTrace(); + throw new IllegalStateException(exc.getMessage()); + } + }else{ + return insets.bottom+15; + } + } + + public int getUnitIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); + } + + public int getBlockIncrement(int direction) { + JViewport vp = myScrollPane.getViewport(); + Rectangle vr = vp.getViewRect(); + return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); + } + } + + private MyEditable getViewer() { + if (myEditable == null) { + myEditable = new MyEditable(); + } + return myEditable; + } + + public CopyProvider getCopyProvider() { + return getViewer(); + } + + public CutProvider getCutProvider() { + return getViewer(); + } + + public PasteProvider getPasteProvider() { + + return getViewer(); + } + + public DeleteProvider getDeleteProvider() { + return getViewer(); + } + + private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { + public void performCopy(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); + } + + public boolean isCopyEnabled(DataContext dataContext) { + return true; + } + + public void performCut(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); + } + + public boolean isCutEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void performPaste(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); + } + + public boolean isPasteEnabled(DataContext dataContext) { + return !isViewer() && getDocument().isWritable(); + } + + public void deleteElement(DataContext dataContext) { + executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); + } + + public boolean canDeleteElement() { + return isViewer() && getDocument().isWritable(); + } + + private void executeAction(String actionId, DataContext dataContext) { + EditorAction action = (EditorAction) ActionManager.getInstance().getAction(actionId); + if (action != null) { + action.actionPerformed(EditorImpl.this, dataContext); + } + } + } + + public void setColorsScheme(EditorColorsScheme scheme) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScheme = scheme; + reinitSettings(); + } + + public EditorColorsScheme getColorsScheme() { + ApplicationManager.getApplication().assertIsDispatchThread(); + return myScheme; + } + + public void setVerticalScrollbarOrientation(int type) { + ApplicationManager.getApplication().assertIsDispatchThread(); + myScrollbarOrientation = type; + if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) { + myScrollPane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); + } else { + myScrollPane.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + } + } + + public void setVerticalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + } + + public void setHorizontalScrollbarVisible(boolean b) { + if (b) { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + else { + myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + } + + public int getVerticalScrollbarOrientation() { + return myScrollbarOrientation; + } + + public MyScrollBar getVerticalScrollBar() { + return myVerticalScrollBar; + } + + public JPanel getPanel() { + return myPanel; + } + + private int getMouseSelectionState() { + return myMouseSelectionState; + } + + private void setMouseSelectionState(int mouseSelectionState) { + myMouseSelectionState = mouseSelectionState; + myMouseSelectionChangeTimestamp = System.currentTimeMillis(); + } + + + public void replaceInputMethodText(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.replaceInputMethodText(e); + } + + public void inputMethodCaretPositionChanged(InputMethodEvent e) { + getInputMethodRequests(); + myInputMethodRequestsHandler.setInputMethodCaretPosition(e); + } + + public InputMethodRequests getInputMethodRequests() { + if (myInputMethodRequestsHandler == null) { + myInputMethodRequestsHandler = new MyInputMethodHandler(); + myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); + } + return myInputMethodRequestsSwingWrapper; + } + + + private class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { + private InputMethodRequests myDelegate; + + public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { + myDelegate = delegate; + } + + public Rectangle getTextLocation(final TextHitInfo offset) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getTextLocation(offset); + + final Rectangle[] r = new Rectangle[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getTextLocation(offset); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public TextHitInfo getLocationOffset(final int x, final int y) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getLocationOffset(x, y); + + final TextHitInfo[] r = new TextHitInfo[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getLocationOffset(x, y); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getInsertPositionOffset() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getInsertPositionOffset(); + + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getInsertPositionOffset(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, + final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedText(beginIndex, endIndex, attributes); + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public int getCommittedTextLength() { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getCommittedTextLength(); + final int[] r = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getCommittedTextLength(); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { + if (ApplicationManagerEx.getApplicationEx().isDispatchThread()) return myDelegate.getSelectedText(attributes); + + final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + r[0] = myDelegate.getSelectedText(attributes); + } + }); + } + catch (InterruptedException e) { + LOG.error(e); + } + catch (InvocationTargetException e) { + LOG.error(e); + } + return r[0]; + } + } + + private class MyInputMethodHandler implements InputMethodRequests { + private String composedText; + private int composedTextStart; + private int composedTextEnd; + + public Rectangle getTextLocation(TextHitInfo offset) { + Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); + Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); + Point p = getContentComponent().getLocationOnScreen(); + r.translate(p.x, p.y); + + return r; + } + + public TextHitInfo getLocationOffset(int x, int y) { + if (composedText != null) { + Point p = getContentComponent().getLocationOnScreen(); + p.x = x - p.x; + p.y = y - p.y; + int pos = logicalPositionToOffset(xyToLogicalPosition(p)); + if ((pos >= composedTextStart) && (pos <= composedTextEnd)) { + return TextHitInfo.leading(pos - composedTextStart); + } + } + return null; + } + + public int getInsertPositionOffset() { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + int caretIndex = getCaretModel().getOffset(); + + if (caretIndex < composedStartIndex) { + return caretIndex; + } else if (caretIndex < composedEndIndex) { + return composedStartIndex; + } else { + return caretIndex - (composedEndIndex - composedStartIndex); + } + } + + private String getText(int startIdx, int endIdx) { + char[] chars = getDocument().getChars(); + return new String(chars, startIdx, endIdx - startIdx); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, + AttributedCharacterIterator.Attribute[] attributes) { + int composedStartIndex = 0; + int composedEndIndex = 0; + if (composedText != null) { + composedStartIndex = composedTextStart; + composedEndIndex = composedTextEnd; + } + + String committed; + if (beginIndex < composedStartIndex) { + if (endIndex <= composedStartIndex) { + committed = getText(beginIndex, endIndex - beginIndex); + } + else { + int firstPartLength = composedStartIndex - beginIndex; + committed = getText(beginIndex, firstPartLength) + + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); + } + } + else { + committed = getText( + beginIndex + (composedEndIndex - composedStartIndex), + endIndex - beginIndex + ); + } + + return new AttributedString(committed).getIterator(); + } + + public int getCommittedTextLength() { + int length = getDocument().getTextLength(); + if (composedText != null) { + length -= composedText.length(); + } + return length; + } + + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + String text = getSelectionModel().getSelectedText(); + return text == null ? null : new AttributedString(text).getIterator(); + } + + private void createComposedString(int composedIndex, AttributedCharacterIterator text) { + StringBuffer strBuf = new StringBuffer(); + + // create attributed string with no attributes + for (char c = text.setIndex(composedIndex); + c != CharacterIterator.DONE; c = text.next()) { + strBuf.append(c); + } + + composedText = new String(strBuf); + } + private void setInputMethodCaretPosition(InputMethodEvent e) { + if (composedText != null) { + int dot; + dot = composedTextStart; + + TextHitInfo caretPos = e.getCaret(); + if (caretPos != null) { + int index = caretPos.getInsertionIndex(); + dot += index; + } + + getCaretModel().moveToOffset(dot); + getScrollingModel().scrollToCaret(ScrollType.RELATIVE); + } + } + + private void runUndoTransparent(final Runnable runnable) { + UndoManager undoManager = myProject == null ? UndoManager.getGlobalInstance() : UndoManager.getInstance(myProject); + undoManager.runUndoTransparentAction(new Runnable() { + public void run() { + CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(runnable); + } + }, "", null); + } + }); + } + + private void replaceInputMethodText(InputMethodEvent e) { + int commitCount = e.getCommittedCharacterCount(); + AttributedCharacterIterator text = e.getText(); + int composedTextIndex; + + // old composed text deletion + final Document doc = getDocument(); + if (composedText != null) { + runUndoTransparent(new Runnable() { + public void run() { + doc.deleteString(composedTextStart, composedTextEnd); + } + }); + composedText = null; + } + + if (text != null) { + text.first(); + + // committed text insertion + if (commitCount > 0) { + for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { + if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction + processKeyTyped(c); + } + } + } + + // new composed text insertion + composedTextIndex = text.getIndex(); + if (composedTextIndex < text.getEndIndex()) { + createComposedString(composedTextIndex, text); + + runUndoTransparent(new Runnable() { + public void run() { + EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); + } + }); + + composedTextStart = getCaretModel().getOffset(); + composedTextEnd = getCaretModel().getOffset() + composedText.length(); + } + } + } + } + + private class MyMouseAdapter extends MouseAdapter { + public void mousePressed(MouseEvent e) { + runMousePressedCommand(e); + } + + public void mouseReleased(MouseEvent e) { + runMouseReleasedCommand(e); + if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && + Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(getFontMetrics(Font.PLAIN)) && + Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { + runMouseClickedCommand(e); + } + myMousePressedEvent = null; + } + + public void mouseEntered(MouseEvent e){ + runMouseEnteredCommand(e); + } + public void mouseExited(MouseEvent e){ + runMouseExitedCommand(e); + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + public void mouseDragged(MouseEvent e) { + validateMousePointer(e); + runMouseDraggedCommand(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseDragged(e); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseDragged(event); + } + } + + public void mouseMoved(MouseEvent e) { + validateMousePointer(e); + EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); + if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { + myGutterComponent.mouseMoved(e); + } + + if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { + FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); + HintManager.getInstance().getTooltipController().showTooptipByMouseMove(EditorImpl.this, e, fold); + } + + EditorMouseMotionListener[] listeners = (EditorMouseMotionListener[]) myMouseMotionListeners.toArray( + new EditorMouseMotionListener[myMouseMotionListeners.size()] + ); + for (int i = 0; i < listeners.length; i++) { + listeners[i].mouseMoved(event); + } + } + } + + private class MyColorSchemeDelegate implements EditorColorsScheme { + private HashMap myOwnAttributes = new HashMap(); + private HashMap myOwnColors = new HashMap(); + + + private EditorColorsScheme getGlobal() { + return EditorColorsManager.getInstance().getGlobalScheme(); + } + + public String getName() { + return getGlobal().getName(); + } + + public void setName(String name) { + getGlobal().setName(name); + } + + public TextAttributes getAttributes(String key) { + if (myOwnAttributes.containsKey(key)) return (TextAttributes) myOwnAttributes.get(key); + return getGlobal().getAttributes(key); + } + + public void setAttributes(String key, TextAttributes attributes) { + myOwnAttributes.put(key, attributes); + } + + public Color getColor(String key) { + if (myOwnColors.containsKey(key)) return (Color) myOwnColors.get(key); + return getGlobal().getColor(key); + } + + public void setColor(String key, Color color) { + myOwnColors.put(key, color); + + // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit + // settings in this case. + myCaretModel.reinitSettings(); + mySelectionModel.reinitSettings(); + } + + public int getEditorFontSize() { + return getGlobal().getEditorFontSize(); + } + + public void setEditorFontSize(int fontSize) { + getGlobal().setEditorFontSize(fontSize); + } + + public String getEditorFontName() { + return getGlobal().getEditorFontName(); + } + + public void setEditorFontName(String fontName) { + getGlobal().setEditorFontName(fontName); + } + + public Font getFont(EditorFontType key) { + return getGlobal().getFont(key); + } + + public void setFont(EditorFontType key, Font font) { + getGlobal().setFont(key, font); + } + + public float getLineSpacing() { + return getGlobal().getLineSpacing(); + } + + public void setLineSpacing(float lineSpacing) { + getGlobal().setLineSpacing(lineSpacing); + } + + public Object clone() { + return null; + } + + public void readExternal(Element element) throws InvalidDataException { + } + + public void writeExternal(Element element) throws WriteExternalException { + } + } + + private static class MyTransferHandler extends TransferHandler { + private RangeMarker myDraggedRange = null; + + private static Editor getEditor(JComponent comp) { + EditorComponentImpl editorComponent = (EditorComponentImpl) comp; + return editorComponent.getEditor(); + } + + public boolean importData(final JComponent comp, final Transferable t) { + final EditorImpl editor = (EditorImpl) getEditor(comp); + + final int caretOffset = editor.getCaretModel().getOffset(); + if (myDraggedRange != null && + myDraggedRange.getStartOffset() <= caretOffset && + caretOffset < myDraggedRange.getEndOffset()) { + return false; + } + + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); + } + + CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + try { + editor.getSelectionModel().removeSelection(); + if (myDraggedRange != null) { + editor.getCaretModel().moveToOffset(caretOffset); + } + + EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable backup = clipboard.getContents(this); + ClipboardOwner clipboardOwner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + clipboard.setContents(t, clipboardOwner); + + editor.putUserData(LAST_PASTED_REGION, null); + pasteHandler.execute(editor, editor.getDataContext()); + clipboard.setContents(backup, clipboardOwner); + + TextRange range = (TextRange) editor.getUserData(LAST_PASTED_REGION); + if (range != null) { + editor.getCaretModel().moveToOffset(range.getStartOffset()); + editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); + } + } catch (Exception exception) { + LOG.error(exception); + } + } + }); + } + }, "Paste", DND_COMMAND_KEY); + + return true; + } + + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + Editor editor = getEditor(comp); + if (editor.isViewer()) return false; + if (!editor.getDocument().isWritable()) return false; + + for (int i = 0; i < transferFlavors.length; i++) { + DataFlavor transferFlavor = transferFlavors[i]; + if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; + } + + return false; + } + + protected Transferable createTransferable(JComponent c) { + Editor editor = getEditor(c); + String s = editor.getSelectionModel().getSelectedText(); + if (s == null) return null; + int selectionStart = editor.getSelectionModel().getSelectionStart(); + int selectionEnd = editor.getSelectionModel().getSelectionEnd(); + myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); + + return new StringSelection(s); + } + + public int getSourceActions(JComponent c) { + return COPY_OR_MOVE; + } + + protected void exportDone(final JComponent source, Transferable data, int action) { + if (data == null) return; + + if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) { + CommandProcessor.getInstance().executeCommand(((EditorImpl) getEditor(source)).myProject, new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + getEditor(source).getDocument().deleteString(myDraggedRange.getStartOffset(), + myDraggedRange.getEndOffset() + ); + } + }); + } + }, "Move selection", DND_COMMAND_KEY); + } + + myDraggedRange = null; + } + } + + class EditorDocumentAdapter extends DocumentAdapter { + public void beforeDocumentChange(DocumentEvent e) { + beforeChangedUpdate(e); + } + + public void documentChanged(DocumentEvent e) { + changedUpdate(e); + } + } +} diff --git a/platform/platform-tests/testData/diff/abc.txt b/platform/platform-tests/testData/diff/abc.txt new file mode 100644 index 000000000000..e7156d8b223f --- /dev/null +++ b/platform/platform-tests/testData/diff/abc.txt @@ -0,0 +1,15 @@ +class abc { + public void f() { +<<<<<<< abc.txt + def +======= + abc +>>>>>>> 1.14 + int F() { + asdfklasdfl + asdgfsagdfhasfdhg + sdafjklklas + sdafjkllksadf + + } +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/addFile/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/addFile/after/1.txt new file mode 100644 index 000000000000..4fcefbf2acb2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFile/after/1.txt @@ -0,0 +1,3 @@ +One +Two +Three diff --git a/platform/platform-tests/testData/diff/applyPatch/addFile/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFile/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/addFile/apply.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFile/apply.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/platform-tests/testData/diff/applyPatch/addFile/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFile/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/after/1.txt new file mode 100644 index 000000000000..4fcefbf2acb2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/after/1.txt @@ -0,0 +1,3 @@ +One +Two +Three diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/apply.patch b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/apply.patch new file mode 100644 index 000000000000..7e2769dd483f --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/apply.patch @@ -0,0 +1,8 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three +-- +1.7.9.5 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithGitVersion/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/after/1.txt new file mode 100644 index 000000000000..40523d5eba10 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/after/1.txt @@ -0,0 +1,3 @@ +One +Two +Three \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/apply.patch b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/apply.patch new file mode 100644 index 000000000000..7f7814bbbf43 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/apply.patch @@ -0,0 +1,7 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three +\ No newline at end of file \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/addFileWithoutNewlineAtEOF/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/addLastLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/addLastLine/after/1.txt new file mode 100644 index 000000000000..64c739e31220 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLastLine/after/1.txt @@ -0,0 +1,3 @@ +first +third +last diff --git a/platform/platform-tests/testData/diff/applyPatch/addLastLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/addLastLine/apply.patch new file mode 100644 index 000000000000..8ba2d13b909b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLastLine/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/1.txt after +@@ -1,2 +1,3 @@ + first + third ++last diff --git a/platform/platform-tests/testData/diff/applyPatch/addLastLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/addLastLine/before/1.txt new file mode 100644 index 000000000000..1effb1028069 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLastLine/before/1.txt @@ -0,0 +1,2 @@ +first +third diff --git a/platform/platform-tests/testData/diff/applyPatch/addLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/addLine/after/1.txt new file mode 100644 index 000000000000..ff6e6b1a5055 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLine/after/1.txt @@ -0,0 +1,3 @@ +first +second +third diff --git a/platform/platform-tests/testData/diff/applyPatch/addLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/addLine/apply.patch new file mode 100644 index 000000000000..a85b9024375a --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLine/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/1.txt after +@@ -1,2 +1,3 @@ + first ++second + third diff --git a/platform/platform-tests/testData/diff/applyPatch/addLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/addLine/before/1.txt new file mode 100644 index 000000000000..1effb1028069 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/addLine/before/1.txt @@ -0,0 +1,2 @@ +first +third diff --git a/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/after/1.txt new file mode 100644 index 000000000000..1978663eef3b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/after/1.txt @@ -0,0 +1,7 @@ +uno +dos +cuatro +cinco +seis +siete +ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/apply.patch b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/apply.patch new file mode 100644 index 000000000000..a735d9f809f9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/apply.patch @@ -0,0 +1,13 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Tue Nov 21 14:20:19 2006 ++++ after/1.txt Tue Nov 21 14:20:15 2006 +@@ -1,7 +1,7 @@ + uno + dos +-tres + cuatro ++cinco + seis + siete +-octo ++ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/before/1.txt new file mode 100644 index 000000000000..1978663eef3b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/alreadyApplied/before/1.txt @@ -0,0 +1,7 @@ +uno +dos +cuatro +cinco +seis +siete +ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiff/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiff/after/1.txt new file mode 100644 index 000000000000..81f7bed1d21f --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiff/after/1.txt @@ -0,0 +1,6 @@ +one +two +three +cuatro +five +seven diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiff/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextDiff/apply.patch new file mode 100644 index 000000000000..08c21265a898 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiff/apply.patch @@ -0,0 +1,18 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Fri Nov 17 16:41:09 2006 +--- after/1.txt Fri Nov 17 16:40:58 2006 +*************** +*** 1,6 **** + one + three +! four + five +- six + seven +--- 1,6 ---- + one ++ two + three +! cuatro + five + seven diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiff/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiff/before/1.txt new file mode 100644 index 000000000000..dde78455b61d --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiff/before/1.txt @@ -0,0 +1,6 @@ +one +three +four +five +six +seven diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/after/1.txt new file mode 100644 index 000000000000..d903ddf7af6e --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/after/1.txt @@ -0,0 +1,7 @@ +first +second +third +fourth +fifth +sixth +seventh diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/apply.patch new file mode 100644 index 000000000000..90e3a687820f --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/apply.patch @@ -0,0 +1,13 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Fri Nov 17 15:30:30 2006 +--- after/1.txt Fri Nov 17 15:30:38 2006 +*************** +*** 1,6 **** +--- 1,7 ---- + first + second + third ++ fourth + fifth + sixth + seventh diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/before/1.txt new file mode 100644 index 000000000000..2b1776423334 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffAddLine/before/1.txt @@ -0,0 +1,6 @@ +first +second +third +fifth +sixth +seventh diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/1.txt new file mode 100644 index 000000000000..ff6e6b1a5055 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/1.txt @@ -0,0 +1,3 @@ +first +second +third diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/2.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/2.txt new file mode 100644 index 000000000000..15bf6080d09b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/after/2.txt @@ -0,0 +1,3 @@ +uno +dos +tres diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/apply.patch new file mode 100644 index 000000000000..c3b0c4d8914a --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/apply.patch @@ -0,0 +1,19 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Tue Nov 07 13:32:58 2006 +--- after/1.txt Tue Nov 07 13:32:58 2006 +*************** +*** 1,2 **** +--- 1,3 ---- + first ++ second + third +diff -r -c before/2.txt after/2.txt +*** before/2.txt Fri Nov 17 18:26:49 2006 +--- after/2.txt Fri Nov 17 18:26:38 2006 +*************** +*** 1,4 **** + uno + dos + tres +- cuatro +--- 1,3 ---- diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/1.txt new file mode 100644 index 000000000000..1effb1028069 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/1.txt @@ -0,0 +1,2 @@ +first +third diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/2.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/2.txt new file mode 100644 index 000000000000..8c857923b895 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffMultiFile/before/2.txt @@ -0,0 +1,4 @@ +uno +dos +tres +cuatro diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/after/1.txt new file mode 100644 index 000000000000..2b1776423334 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/after/1.txt @@ -0,0 +1,6 @@ +first +second +third +fifth +sixth +seventh diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/apply.patch new file mode 100644 index 000000000000..eb94f6833630 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/apply.patch @@ -0,0 +1,13 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Fri Nov 17 16:29:29 2006 +--- after/1.txt Fri Nov 17 16:29:26 2006 +*************** +*** 1,7 **** + first + second + third +- fourth + fifth + sixth + seventh +--- 1,6 ---- diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/before/1.txt new file mode 100644 index 000000000000..d903ddf7af6e --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffRemoveLine/before/1.txt @@ -0,0 +1,7 @@ +first +second +third +fourth +fifth +sixth +seventh diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/after/1.txt new file mode 100644 index 000000000000..d549733a1b50 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/after/1.txt @@ -0,0 +1,11 @@ +uno + + +dos +tres + + +cuatro + + +cinco diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/apply.patch new file mode 100644 index 000000000000..135da5647e61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/apply.patch @@ -0,0 +1,13 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Tue Nov 21 18:10:18 2006 +--- after/1.txt Tue Nov 21 18:10:26 2006 +*************** +*** 2,7 **** +--- 2,8 ---- + + + dos ++ tres + + + cuatro diff --git a/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/before/1.txt new file mode 100644 index 000000000000..b3a6b02feeb2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextDiffSingleSpace/before/1.txt @@ -0,0 +1,10 @@ +uno + + +dos + + +cuatro + + +cinco diff --git a/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/after/1.txt new file mode 100644 index 000000000000..3664b17e2df8 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/after/1.txt @@ -0,0 +1,3 @@ +uno +dos +tres \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/apply.patch b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/apply.patch new file mode 100644 index 000000000000..6889e408241c --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/apply.patch @@ -0,0 +1,13 @@ +diff -r -c before/1.txt after/1.txt +*** before/1.txt Fri Nov 24 13:06:16 2006 +--- after/1.txt Fri Nov 24 13:06:20 2006 +*************** +*** 1,2 **** + uno +! dos +\ No newline at end of file +--- 1,3 ---- + uno +! dos +! tres +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/before/1.txt new file mode 100644 index 000000000000..8306d505b4d9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/contextNoNewlineAtEof/before/1.txt @@ -0,0 +1,2 @@ +uno +dos \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/after/1.txt new file mode 100644 index 000000000000..24d6b3a04df7 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/after/1.txt @@ -0,0 +1,15 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/apply.patch b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/apply.patch new file mode 100644 index 000000000000..dacecb95b351 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/apply.patch @@ -0,0 +1,13 @@ +diff --git a/before/1.txt b/after/1.txt +index fad3998..5880be4 100644 +--- a/before/1.txt ++++ b/after/1.txt +@@ -12,5 +12,4 @@ class Test { + }; + } + +- void b(Supplier s) {} +-} +\ No newline at end of file ++ void b(Supplier s) {} +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/before/1.txt new file mode 100644 index 000000000000..fad399888133 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithLineBreak/before/1.txt @@ -0,0 +1,16 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/after/1.txt new file mode 100644 index 000000000000..bf3ee82452a0 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/after/1.txt @@ -0,0 +1,15 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/apply.patch b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/apply.patch new file mode 100644 index 000000000000..6c5569b931f1 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/apply.patch @@ -0,0 +1,10 @@ +diff --git a/before/1.txt b/after/1.txt +index fad3998..5880be4 100644 +--- a/before/1.txt ++++ b/after/1.txt +@@ -13,4 +13,3 @@ + } + + void b(Supplier s) {} +-} +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/before/1.txt new file mode 100644 index 000000000000..fad399888133 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLastLineWithoutLineBreak/before/1.txt @@ -0,0 +1,16 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/after/1.txt new file mode 100644 index 000000000000..1b08699f2ea8 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/after/1.txt @@ -0,0 +1,16 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/apply.patch b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/apply.patch new file mode 100644 index 000000000000..399055363976 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/apply.patch @@ -0,0 +1,12 @@ +diff --git a/before/1.txt b/after/1.txt +index fad3998..5880be4 100644 +--- a/before/1.txt ++++ b/after/1.txt +@@ -12,5 +12,5 @@ + }; + } + +- void b(Supplier s) {} ++ + } +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/before/1.txt new file mode 100644 index 000000000000..fad399888133 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/deleteLineContentWithoutLineBreak/before/1.txt @@ -0,0 +1,16 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/emptyLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/emptyLine/after/1.txt new file mode 100644 index 000000000000..8d69aa718185 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/emptyLine/after/1.txt @@ -0,0 +1,9 @@ +uno + +dos +tres + +cuarto + +cinco +seis diff --git a/platform/platform-tests/testData/diff/applyPatch/emptyLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/emptyLine/apply.patch new file mode 100644 index 000000000000..55b52234186f --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/emptyLine/apply.patch @@ -0,0 +1,11 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Mon Nov 20 14:37:58 2006 ++++ after/1.txt Mon Nov 20 14:38:07 2006 +@@ -3,6 +3,7 @@ + dos + tres + ++cuarto + + cinco + seis diff --git a/platform/platform-tests/testData/diff/applyPatch/emptyLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/emptyLine/before/1.txt new file mode 100644 index 000000000000..77808628e231 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/emptyLine/before/1.txt @@ -0,0 +1,8 @@ +uno + +dos +tres + + +cinco +seis diff --git a/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/after/ResolveCache.java b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/after/ResolveCache.java new file mode 100644 index 000000000000..939aef6384c5 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/after/ResolveCache.java @@ -0,0 +1,262 @@ +package com.intellij.psi.impl.source.resolve; + +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.util.Key; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiManagerImpl; +import com.intellij.reference.SoftReference; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ConcurrentWeakHashMap; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class ResolveCache { + private static final Key>> JAVA_RESOLVE_MAP = Key.create("ResolveCache.JAVA_RESOLVE_MAP"); + private static final Key>> RESOLVE_MAP = Key.create("ResolveCache.RESOLVE_MAP"); + private static final Key>> JAVA_RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.JAVA_RESOLVE_MAP_INCOMPLETE"); + private static final Key>> RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.RESOLVE_MAP_INCOMPLETE"); + private static final Key> IS_BEING_RESOLVED_KEY = Key.create("ResolveCache.IS_BEING_RESOLVED_KEY"); + private static final Key> VAR_TO_CONST_VALUE_MAP_KEY = Key.create("ResolveCache.VAR_TO_CONST_VALUE_MAP_KEY"); + + //store types for method call expressions, NB: this caching is semantical, without this captured wildcards won't work + private final ConcurrentWeakHashMap> myCaclulatedlTypes = new ConcurrentWeakHashMap>(); + + private static final Object NULL = Key.create("NULL"); + + private final PsiManagerImpl myManager; + + private final Map myVarToConstValueMap1; + private final Map myVarToConstValueMap2; + + private final Map>[] myPolyVariantResolveMaps = new Map[4]; + private final Map>[] myResolveMaps = new Map[4]; + private final AtomicInteger myClearCount = new AtomicInteger(0); + + + public static interface AbstractResolver { + Result resolve(Ref ref, boolean incompleteCode); + } + public static interface PolyVariantResolver extends AbstractResolver { + } + + public static interface Resolver extends AbstractResolver{ + } + + public ResolveCache(PsiManagerImpl manager) { + myManager = manager; + + myVarToConstValueMap1 = getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, true); + myVarToConstValueMap2 = getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, false); + + myPolyVariantResolveMaps[0] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true); + myPolyVariantResolveMaps[1] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true); + myResolveMaps[0] = getOrCreateWeakMap(myManager, RESOLVE_MAP, true); + myResolveMaps[1] = getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true); + + myPolyVariantResolveMaps[2] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false); + myPolyVariantResolveMaps[3] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false); + + myResolveMaps[2] = getOrCreateWeakMap(myManager, RESOLVE_MAP, false); + myResolveMaps[3] = getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false); + + myManager.registerRunnableToRunOnAnyChange(new Runnable() { + public void run() { + myCaclulatedlTypes.clear(); + } + }); + } + + public PsiType getType(PsiExpression expr, Function f) { + WeakReference ref = myCaclulatedlTypes.get(expr); + PsiType type = ref == null ? null : ref.get(); + if (type == null) { + type = f.fun(expr); + WeakReference existingRef = ConcurrencyUtil.cacheOrGet(myCaclulatedlTypes, expr, new WeakReference(type)); + PsiType existing = existingRef.get(); + if (existing != null) type = existing; + } + assert type == null || type.isValid(); + return type; + } + + public void clearCache() { + myClearCount.incrementAndGet(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP, true).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP, false).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false).clear(); + } + + private Result resolve(Ref ref, + AbstractResolver resolver, + Map>[] maps, + boolean needToPreventRecursion, + boolean incompleteCode) { + ProgressManager.getInstance().checkCanceled(); + + int clearCountOnStart = myClearCount.intValue(); + + boolean physical = ref.getElement().isPhysical(); + Result result = getCached(ref, maps, physical, incompleteCode); + if (result != null) { + return result; + } + + if (incompleteCode) { + result = resolve(ref, resolver, maps, needToPreventRecursion, false); + if (result != null && !(result instanceof Object[] && ((Object[])result).length == 0)) { + cache(ref, result, maps, physical, incompleteCode, clearCountOnStart); + return result; + } + } + + if (needToPreventRecursion && !lockElement(ref)) return null; + try { + result = resolver.resolve(ref, incompleteCode); + } + finally { + if (needToPreventRecursion) { + unlockElement(ref); + } + } + cache(ref, result, maps, physical, incompleteCode, clearCountOnStart); + return result; + } + + public ResolveResult[] resolveWithCaching(PsiPolyVariantReference ref, + PolyVariantResolver resolver, + boolean needToPreventRecursion, + boolean incompleteCode) { + ResolveResult[] result = resolve(ref, resolver, myPolyVariantResolveMaps, needToPreventRecursion, incompleteCode); + return result == null ? JavaResolveResult.EMPTY_ARRAY : result; + } + + public PsiElement resolveWithCaching(PsiReference ref, + Resolver resolver, + boolean needToPreventRecursion, + boolean incompleteCode) { + return resolve(ref, resolver, myResolveMaps, needToPreventRecursion, incompleteCode); + } + + private static boolean lockElement(PsiReference ref) { + synchronized (IS_BEING_RESOLVED_KEY) { + PsiElement elt = ref.getElement(); + + List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); + final Thread currentThread = Thread.currentThread(); + if (lockingThreads == null) { + lockingThreads = new ArrayList(1); + elt.putUserData(IS_BEING_RESOLVED_KEY, lockingThreads); + } + else { + if (lockingThreads.contains(currentThread)) return false; + } + lockingThreads.add(currentThread); + } + return true; + } + + private static void unlockElement(PsiReference ref) { + synchronized (IS_BEING_RESOLVED_KEY) { + PsiElement elt = ref.getElement(); + + List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); + if (lockingThreads == null) return; + final Thread currentThread = Thread.currentThread(); + lockingThreads.remove(currentThread); + if (lockingThreads.isEmpty()) { + elt.putUserData(IS_BEING_RESOLVED_KEY, null); + } + } + } + + //for Visual Fabrique + public void clearResolveCaches(PsiReference ref) { + myClearCount.incrementAndGet(); + final boolean physical = ref.getElement().isPhysical(); + if (ref instanceof PsiPolyVariantReference) { + cache((PsiPolyVariantReference)ref, null, myPolyVariantResolveMaps, physical, false, myClearCount.intValue()); + cache((PsiPolyVariantReference)ref, null, myPolyVariantResolveMaps, physical, true, myClearCount.intValue()); + } + } + + + private static int getIndex(boolean physical, boolean ic){ + return (physical ? 0 : 1) << 1 | (ic ? 1 : 0); + } + + private static Result getCached(Ref ref, Map>[] maps, boolean physical, boolean ic){ + int index = getIndex(physical, ic); + Reference reference = maps[index].get(ref); + if(reference == null) return null; + return reference.get(); + } + private void cache(Ref ref, Result result, Map>[] maps, boolean physical, boolean incompleteCode, final int clearCountOnStart) { + if (clearCountOnStart != myClearCount.intValue() && result != null) return; + + int index = getIndex(physical, incompleteCode); + maps[index].put(ref, new SoftReference(result)); + } + + public static interface ConstValueComputer{ + Object execute(PsiVariable variable, Set visitedVars); + } + + public Object computeConstantValueWithCaching(PsiVariable variable, ConstValueComputer computer, Set visitedVars){ + boolean physical = variable.isPhysical(); + + Object cached = (physical ? myVarToConstValueMap1 : myVarToConstValueMap2).get(variable); + if (cached == NULL) return null; + if (cached != null) return cached; + + Object result = computer.execute(variable, visitedVars); + + (physical ? myVarToConstValueMap1 : myVarToConstValueMap2).put(variable, result != null ? result : NULL); + + return result; + } + + public ConcurrentMap getOrCreateWeakMap(final PsiManagerImpl manager, final Key> key, boolean forPhysical) { + MapPair pair = manager.getUserData(key); + if (pair == null){ + pair = new MapPair(); + pair = manager.putUserDataIfAbsent(key, pair); + + final MapPair _pair = pair; + manager.registerRunnableToRunOnChange( + new Runnable() { + public void run() { + myClearCount.incrementAndGet(); + _pair.physicalMap.clear(); + } + } + ); + manager.registerRunnableToRunOnAnyChange( + new Runnable() { + public void run() { + myClearCount.incrementAndGet(); + _pair.nonPhysicalMap.clear(); + } + } + ); + } + return forPhysical ? pair.physicalMap : pair.nonPhysicalMap; + } + + public static class MapPair{ + public final ConcurrentMap physicalMap = new ConcurrentWeakHashMap(); + public final ConcurrentMap nonPhysicalMap = new ConcurrentWeakHashMap(); + } +} diff --git a/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/apply.patch b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/apply.patch new file mode 100644 index 000000000000..ce8873912dfb --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/apply.patch @@ -0,0 +1,430 @@ +--- before/ResolveCache.java (revision 1) ++++ after/ResolveCache.java Thu Feb 01 20:27:02 MSK 2007 +@@ -5,23 +5,29 @@ + import com.intellij.psi.*; + import com.intellij.psi.impl.PsiManagerImpl; + import com.intellij.reference.SoftReference; ++import com.intellij.util.ConcurrencyUtil; + import com.intellij.util.Function; +-import com.intellij.util.containers.WeakHashMap; ++import com.intellij.util.containers.ConcurrentWeakHashMap; + + import java.lang.ref.Reference; + import java.lang.ref.WeakReference; +-import java.util.*; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.concurrent.ConcurrentMap; ++import java.util.concurrent.atomic.AtomicInteger; + + public class ResolveCache { +- private static final Key>> JAVA_RESOLVE_MAP = Key.create("ResolveCache.JAVA_RESOLVE_MAP"); ++ private static final Key>> JAVA_RESOLVE_MAP = Key.create("ResolveCache.JAVA_RESOLVE_MAP"); + private static final Key>> RESOLVE_MAP = Key.create("ResolveCache.RESOLVE_MAP"); +- private static final Key>> JAVA_RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.JAVA_RESOLVE_MAP_INCOMPLETE"); ++ private static final Key>> JAVA_RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.JAVA_RESOLVE_MAP_INCOMPLETE"); + private static final Key>> RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.RESOLVE_MAP_INCOMPLETE"); + private static final Key> IS_BEING_RESOLVED_KEY = Key.create("ResolveCache.IS_BEING_RESOLVED_KEY"); + private static final Key> VAR_TO_CONST_VALUE_MAP_KEY = Key.create("ResolveCache.VAR_TO_CONST_VALUE_MAP_KEY"); + + //store types for method call expressions, NB: this caching is semantical, without this captured wildcards won't work +- private Map> myCaclulatedlTypes; ++ private final ConcurrentWeakHashMap> myCaclulatedlTypes = new ConcurrentWeakHashMap>(); + + private static final Object NULL = Key.create("NULL"); + +@@ -30,24 +36,25 @@ + private final Map myVarToConstValueMap1; + private final Map myVarToConstValueMap2; + +- private final WeakHashMap[] myPolyVariantResolveMaps = new WeakHashMap[4]; +- private final WeakHashMap[] myResolveMaps = new WeakHashMap[4]; +- private int myClearCount = 0; ++ private final Map>[] myPolyVariantResolveMaps = new Map[4]; ++ private final Map>[] myResolveMaps = new Map[4]; ++ private final AtomicInteger myClearCount = new AtomicInteger(0); + + +- public static interface PolyVariantResolver { +- ResolveResult[] resolve(PsiPolyVariantReference ref, boolean incompleteCode); ++ public static interface AbstractResolver { ++ Result resolve(Ref ref, boolean incompleteCode); + } ++ public static interface PolyVariantResolver extends AbstractResolver { ++ } + +- public static interface Resolver{ +- PsiElement resolve(PsiReference ref, boolean incompleteCode); ++ public static interface Resolver extends AbstractResolver{ + } + + public ResolveCache(PsiManagerImpl manager) { + myManager = manager; + +- myVarToConstValueMap1 = Collections.synchronizedMap(getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, true)); +- myVarToConstValueMap2 = Collections.synchronizedMap(getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, false)); ++ myVarToConstValueMap1 = getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, true); ++ myVarToConstValueMap2 = getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, false); + + myPolyVariantResolveMaps[0] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true); + myPolyVariantResolveMaps[1] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true); +@@ -60,200 +67,147 @@ + myResolveMaps[2] = getOrCreateWeakMap(myManager, RESOLVE_MAP, false); + myResolveMaps[3] = getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false); + +- myCaclulatedlTypes = new WeakHashMap>(); + myManager.registerRunnableToRunOnAnyChange(new Runnable() { + public void run() { +- synchronized (PsiLock.LOCK) { +- myCaclulatedlTypes.clear(); +- } ++ myCaclulatedlTypes.clear(); ++ } +- } + }); + } + + public PsiType getType(PsiExpression expr, Function f) { +- WeakReference ref; +- synchronized (PsiLock.LOCK) { +- ref = myCaclulatedlTypes.get(expr); +- } ++ WeakReference ref = myCaclulatedlTypes.get(expr); + PsiType type = ref == null ? null : ref.get(); + if (type == null) { + type = f.fun(expr); +- synchronized (PsiLock.LOCK) { +- myCaclulatedlTypes.put(expr, new WeakReference(type)); ++ WeakReference existingRef = ConcurrencyUtil.cacheOrGet(myCaclulatedlTypes, expr, new WeakReference(type)); ++ PsiType existing = existingRef.get(); ++ if (existing != null) type = existing; +- } ++ } +- } +- ++ assert type == null || type.isValid(); + return type; + } + + public void clearCache() { +- synchronized (PsiLock.LOCK) { +- myClearCount++; ++ myClearCount.incrementAndGet(); +- getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true).clear(); +- getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true).clear(); +- getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false).clear(); +- getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false).clear(); +- getOrCreateWeakMap(myManager, RESOLVE_MAP, true).clear(); +- getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true).clear(); +- getOrCreateWeakMap(myManager, RESOLVE_MAP, false).clear(); +- getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false).clear(); +- } ++ getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true).clear(); ++ getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true).clear(); ++ getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false).clear(); ++ getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false).clear(); ++ getOrCreateWeakMap(myManager, RESOLVE_MAP, true).clear(); ++ getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true).clear(); ++ getOrCreateWeakMap(myManager, RESOLVE_MAP, false).clear(); ++ getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false).clear(); ++ } +- } + +- public PsiElement resolveWithCaching(PsiReference ref, +- Resolver resolver, ++ private Result resolve(Ref ref, ++ AbstractResolver resolver, ++ Map>[] maps, +- boolean needToPreventRecursion, +- boolean incompleteCode) { ++ boolean needToPreventRecursion, ++ boolean incompleteCode) { + ProgressManager.getInstance().checkCanceled(); + +- int clearCountOnStart; +- synchronized (PsiLock.LOCK) { +- clearCountOnStart = myClearCount; +- } ++ int clearCountOnStart = myClearCount.intValue(); + + boolean physical = ref.getElement().isPhysical(); +- final Reference cached = getCachedResolve(ref, physical, incompleteCode); +- if (cached != null) return cached.get(); ++ Result result = getCached(ref, maps, physical, incompleteCode); ++ if (result != null) { ++ return result; ++ } +- ++ + if (incompleteCode) { +- final PsiElement results = resolveWithCaching(ref, resolver, needToPreventRecursion, false); +- if (results != null) { +- setCachedResolve(ref, results, physical, true, clearCountOnStart); +- return results; ++ result = resolve(ref, resolver, maps, needToPreventRecursion, false); ++ if (result != null && !(result instanceof Object[] && ((Object[])result).length == 0)) { ++ cache(ref, result, maps, physical, incompleteCode, clearCountOnStart); ++ return result; + } + } + +- if (!lockElement(ref, needToPreventRecursion)) return null; +- PsiElement result = null; ++ if (needToPreventRecursion && !lockElement(ref)) return null; + try { + result = resolver.resolve(ref, incompleteCode); + } +- finally{ ++ finally { +- unlockElement(ref, needToPreventRecursion); ++ if (needToPreventRecursion) { ++ unlockElement(ref); +- } ++ } +- +- setCachedResolve(ref, result, physical, incompleteCode, clearCountOnStart); ++ } ++ cache(ref, result, maps, physical, incompleteCode, clearCountOnStart); + return result; + } + +- private static boolean lockElement(PsiReference ref, boolean doLock) { +- if (doLock) { ++ public ResolveResult[] resolveWithCaching(PsiPolyVariantReference ref, ++ PolyVariantResolver resolver, ++ boolean needToPreventRecursion, ++ boolean incompleteCode) { ++ ResolveResult[] result = resolve(ref, resolver, myPolyVariantResolveMaps, needToPreventRecursion, incompleteCode); ++ return result == null ? JavaResolveResult.EMPTY_ARRAY : result; ++ } ++ ++ public PsiElement resolveWithCaching(PsiReference ref, ++ Resolver resolver, ++ boolean needToPreventRecursion, ++ boolean incompleteCode) { ++ return resolve(ref, resolver, myResolveMaps, needToPreventRecursion, incompleteCode); ++ } ++ ++ private static boolean lockElement(PsiReference ref) { +- synchronized (IS_BEING_RESOLVED_KEY) { +- PsiElement elt = ref.getElement(); ++ synchronized (IS_BEING_RESOLVED_KEY) { ++ PsiElement elt = ref.getElement(); + +- List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); +- final Thread currentThread = Thread.currentThread(); +- if (lockingThreads == null) { +- lockingThreads = new ArrayList(1); +- elt.putUserData(IS_BEING_RESOLVED_KEY, lockingThreads); +- } +- else { +- if (lockingThreads.contains(currentThread)) return false; +- } +- lockingThreads.add(currentThread); +- } ++ List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); ++ final Thread currentThread = Thread.currentThread(); ++ if (lockingThreads == null) { ++ lockingThreads = new ArrayList(1); ++ elt.putUserData(IS_BEING_RESOLVED_KEY, lockingThreads); ++ } ++ else { ++ if (lockingThreads.contains(currentThread)) return false; ++ } ++ lockingThreads.add(currentThread); ++ } +- } + return true; + } + +- private static void unlockElement(PsiReference ref, boolean doLock) { +- if (doLock) { ++ private static void unlockElement(PsiReference ref) { +- synchronized (IS_BEING_RESOLVED_KEY) { +- PsiElement elt = ref.getElement(); ++ synchronized (IS_BEING_RESOLVED_KEY) { ++ PsiElement elt = ref.getElement(); + +- List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); +- if (lockingThreads == null) return; +- final Thread currentThread = Thread.currentThread(); +- lockingThreads.remove(currentThread); +- if (lockingThreads.isEmpty()) { +- elt.putUserData(IS_BEING_RESOLVED_KEY, null); +- } +- } +- } ++ List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); ++ if (lockingThreads == null) return; ++ final Thread currentThread = Thread.currentThread(); ++ lockingThreads.remove(currentThread); ++ if (lockingThreads.isEmpty()) { ++ elt.putUserData(IS_BEING_RESOLVED_KEY, null); ++ } ++ } ++ } +- } + +- private void setCachedResolve(PsiReference ref, PsiElement results, boolean physical, boolean incompleteCode, final int clearCountOnStart) { +- synchronized (PsiLock.LOCK) { +- if (clearCountOnStart != myClearCount && results != null) return; +- +- int index = getIndex(physical, incompleteCode); +- myResolveMaps[index].put(ref, new SoftReference(results)); +- } +- } +- + //for Visual Fabrique + public void clearResolveCaches(PsiReference ref) { +- synchronized (PsiLock.LOCK) { +- myClearCount++; ++ myClearCount.incrementAndGet(); +- final boolean physical = ref.getElement().isPhysical(); ++ final boolean physical = ref.getElement().isPhysical(); +- setCachedPolyVariantResolve(ref, null, physical, false, myClearCount); +- setCachedPolyVariantResolve(ref, null, physical, true, myClearCount); ++ if (ref instanceof PsiPolyVariantReference) { ++ cache((PsiPolyVariantReference)ref, null, myPolyVariantResolveMaps, physical, false, myClearCount.intValue()); ++ cache((PsiPolyVariantReference)ref, null, myPolyVariantResolveMaps, physical, true, myClearCount.intValue()); + } + } + +- private Reference getCachedResolve(PsiReference ref, boolean physical, boolean incompleteCode) { +- synchronized (PsiLock.LOCK) { +- int index = getIndex(physical, incompleteCode); +- final Reference reference = (Reference)myResolveMaps[index].get(ref); +- if(reference == null) return null; +- return reference; +- } +- } + +- public ResolveResult[] resolveWithCaching(PsiPolyVariantReference ref, +- PolyVariantResolver resolver, +- boolean needToPreventRecursion, +- boolean incompleteCode) { +- ProgressManager.getInstance().checkCanceled(); +- +- int clearCountOnStart; +- synchronized (PsiLock.LOCK) { +- clearCountOnStart = myClearCount; +- } +- +- boolean physical = ref.getElement().isPhysical(); +- final ResolveResult[] cached = getCachedPolyVariantResolve(ref, physical, incompleteCode); +- if (cached != null) return cached; +- +- if (incompleteCode) { +- final ResolveResult[] results = resolveWithCaching(ref, resolver, needToPreventRecursion, false); +- if (results != null && results.length > 0) { +- setCachedPolyVariantResolve(ref, results, physical, true, clearCountOnStart); +- return results; +- } +- } +- +- if (!lockElement(ref, needToPreventRecursion)) return JavaResolveResult.EMPTY_ARRAY; +- ResolveResult[] result; +- try { +- result = resolver.resolve(ref, incompleteCode); +- } finally { +- unlockElement(ref, needToPreventRecursion); +- } +- +- setCachedPolyVariantResolve(ref, result, physical, incompleteCode, clearCountOnStart); +- return result; +- } +- + private static int getIndex(boolean physical, boolean ic){ + return (physical ? 0 : 1) << 1 | (ic ? 1 : 0); + } + +- private void setCachedPolyVariantResolve(PsiReference ref, ResolveResult[] result, boolean physical, boolean incomplete, int clearCountOnStart){ +- synchronized (PsiLock.LOCK) { +- if (clearCountOnStart != myClearCount && result != null) return; +- int index = getIndex(physical, incomplete); +- myPolyVariantResolveMaps[index].put(ref, new SoftReference(result)); +- } +- } +- +- private ResolveResult[] getCachedPolyVariantResolve(PsiReference ref, boolean physical, boolean ic){ +- synchronized (PsiLock.LOCK) { ++ private static Result getCached(Ref ref, Map>[] maps, boolean physical, boolean ic){ +- int index = getIndex(physical, ic); ++ int index = getIndex(physical, ic); +- final Reference reference = (Reference)myPolyVariantResolveMaps[index].get(ref); ++ Reference reference = maps[index].get(ref); +- if(reference == null) return null; +- return reference.get(); +- } ++ if(reference == null) return null; ++ return reference.get(); ++ } ++ private void cache(Ref ref, Result result, Map>[] maps, boolean physical, boolean incompleteCode, final int clearCountOnStart) { ++ if (clearCountOnStart != myClearCount.intValue() && result != null) return; ++ ++ int index = getIndex(physical, incompleteCode); ++ maps[index].put(ref, new SoftReference(result)); + } + + public static interface ConstValueComputer{ +@@ -274,44 +228,35 @@ + return result; + } + +- public WeakHashMap getOrCreateWeakMap(final PsiManagerImpl manager, final Key> key, boolean forPhysical) { ++ public ConcurrentMap getOrCreateWeakMap(final PsiManagerImpl manager, final Key> key, boolean forPhysical) { + MapPair pair = manager.getUserData(key); + if (pair == null){ + pair = new MapPair(); +- manager.putUserData(key, pair); ++ pair = manager.putUserDataIfAbsent(key, pair); + + final MapPair _pair = pair; + manager.registerRunnableToRunOnChange( + new Runnable() { + public void run() { +- synchronized (PsiLock.LOCK) { +- myClearCount++; ++ myClearCount.incrementAndGet(); +- _pair.physicalMap.clear(); +- } +- } ++ _pair.physicalMap.clear(); ++ } ++ } +- } + ); + manager.registerRunnableToRunOnAnyChange( + new Runnable() { + public void run() { +- synchronized (PsiLock.LOCK) { +- myClearCount++; ++ myClearCount.incrementAndGet(); +- _pair.nonPhysicalMap.clear(); +- } +- } ++ _pair.nonPhysicalMap.clear(); ++ } ++ } +- } + ); + } + return forPhysical ? pair.physicalMap : pair.nonPhysicalMap; + } + + public static class MapPair{ +- public WeakHashMap physicalMap; +- public WeakHashMap nonPhysicalMap; +- +- public MapPair() { +- physicalMap = new WeakHashMap(); +- nonPhysicalMap = new WeakHashMap(); ++ public final ConcurrentMap physicalMap = new ConcurrentWeakHashMap(); ++ public final ConcurrentMap nonPhysicalMap = new ConcurrentWeakHashMap(); +- } +- } ++ } ++} +\ No newline at end of file +-} diff --git a/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/before/ResolveCache.java b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/before/ResolveCache.java new file mode 100644 index 000000000000..0b3ce28385c5 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/incorrectAlreadyAppliedDetection/before/ResolveCache.java @@ -0,0 +1,317 @@ +package com.intellij.psi.impl.source.resolve; + +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.util.Key; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiManagerImpl; +import com.intellij.reference.SoftReference; +import com.intellij.util.Function; +import com.intellij.util.containers.WeakHashMap; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.*; + +public class ResolveCache { + private static final Key>> JAVA_RESOLVE_MAP = Key.create("ResolveCache.JAVA_RESOLVE_MAP"); + private static final Key>> RESOLVE_MAP = Key.create("ResolveCache.RESOLVE_MAP"); + private static final Key>> JAVA_RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.JAVA_RESOLVE_MAP_INCOMPLETE"); + private static final Key>> RESOLVE_MAP_INCOMPLETE = Key.create("ResolveCache.RESOLVE_MAP_INCOMPLETE"); + private static final Key> IS_BEING_RESOLVED_KEY = Key.create("ResolveCache.IS_BEING_RESOLVED_KEY"); + private static final Key> VAR_TO_CONST_VALUE_MAP_KEY = Key.create("ResolveCache.VAR_TO_CONST_VALUE_MAP_KEY"); + + //store types for method call expressions, NB: this caching is semantical, without this captured wildcards won't work + private Map> myCaclulatedlTypes; + + private static final Object NULL = Key.create("NULL"); + + private final PsiManagerImpl myManager; + + private final Map myVarToConstValueMap1; + private final Map myVarToConstValueMap2; + + private final WeakHashMap[] myPolyVariantResolveMaps = new WeakHashMap[4]; + private final WeakHashMap[] myResolveMaps = new WeakHashMap[4]; + private int myClearCount = 0; + + + public static interface PolyVariantResolver { + ResolveResult[] resolve(PsiPolyVariantReference ref, boolean incompleteCode); + } + + public static interface Resolver{ + PsiElement resolve(PsiReference ref, boolean incompleteCode); + } + + public ResolveCache(PsiManagerImpl manager) { + myManager = manager; + + myVarToConstValueMap1 = Collections.synchronizedMap(getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, true)); + myVarToConstValueMap2 = Collections.synchronizedMap(getOrCreateWeakMap(myManager, VAR_TO_CONST_VALUE_MAP_KEY, false)); + + myPolyVariantResolveMaps[0] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true); + myPolyVariantResolveMaps[1] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true); + myResolveMaps[0] = getOrCreateWeakMap(myManager, RESOLVE_MAP, true); + myResolveMaps[1] = getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true); + + myPolyVariantResolveMaps[2] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false); + myPolyVariantResolveMaps[3] = getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false); + + myResolveMaps[2] = getOrCreateWeakMap(myManager, RESOLVE_MAP, false); + myResolveMaps[3] = getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false); + + myCaclulatedlTypes = new WeakHashMap>(); + myManager.registerRunnableToRunOnAnyChange(new Runnable() { + public void run() { + synchronized (PsiLock.LOCK) { + myCaclulatedlTypes.clear(); + } + } + }); + } + + public PsiType getType(PsiExpression expr, Function f) { + WeakReference ref; + synchronized (PsiLock.LOCK) { + ref = myCaclulatedlTypes.get(expr); + } + PsiType type = ref == null ? null : ref.get(); + if (type == null) { + type = f.fun(expr); + synchronized (PsiLock.LOCK) { + myCaclulatedlTypes.put(expr, new WeakReference(type)); + } + } + + return type; + } + + public void clearCache() { + synchronized (PsiLock.LOCK) { + myClearCount++; + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, true).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, true).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP, false).clear(); + getOrCreateWeakMap(myManager, JAVA_RESOLVE_MAP_INCOMPLETE, false).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP, true).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, true).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP, false).clear(); + getOrCreateWeakMap(myManager, RESOLVE_MAP_INCOMPLETE, false).clear(); + } + } + + public PsiElement resolveWithCaching(PsiReference ref, + Resolver resolver, + boolean needToPreventRecursion, + boolean incompleteCode) { + ProgressManager.getInstance().checkCanceled(); + + int clearCountOnStart; + synchronized (PsiLock.LOCK) { + clearCountOnStart = myClearCount; + } + + boolean physical = ref.getElement().isPhysical(); + final Reference cached = getCachedResolve(ref, physical, incompleteCode); + if (cached != null) return cached.get(); + + if (incompleteCode) { + final PsiElement results = resolveWithCaching(ref, resolver, needToPreventRecursion, false); + if (results != null) { + setCachedResolve(ref, results, physical, true, clearCountOnStart); + return results; + } + } + + if (!lockElement(ref, needToPreventRecursion)) return null; + PsiElement result = null; + try { + result = resolver.resolve(ref, incompleteCode); + } + finally{ + unlockElement(ref, needToPreventRecursion); + } + + setCachedResolve(ref, result, physical, incompleteCode, clearCountOnStart); + return result; + } + + private static boolean lockElement(PsiReference ref, boolean doLock) { + if (doLock) { + synchronized (IS_BEING_RESOLVED_KEY) { + PsiElement elt = ref.getElement(); + + List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); + final Thread currentThread = Thread.currentThread(); + if (lockingThreads == null) { + lockingThreads = new ArrayList(1); + elt.putUserData(IS_BEING_RESOLVED_KEY, lockingThreads); + } + else { + if (lockingThreads.contains(currentThread)) return false; + } + lockingThreads.add(currentThread); + } + } + return true; + } + + private static void unlockElement(PsiReference ref, boolean doLock) { + if (doLock) { + synchronized (IS_BEING_RESOLVED_KEY) { + PsiElement elt = ref.getElement(); + + List lockingThreads = elt.getUserData(IS_BEING_RESOLVED_KEY); + if (lockingThreads == null) return; + final Thread currentThread = Thread.currentThread(); + lockingThreads.remove(currentThread); + if (lockingThreads.isEmpty()) { + elt.putUserData(IS_BEING_RESOLVED_KEY, null); + } + } + } + } + + private void setCachedResolve(PsiReference ref, PsiElement results, boolean physical, boolean incompleteCode, final int clearCountOnStart) { + synchronized (PsiLock.LOCK) { + if (clearCountOnStart != myClearCount && results != null) return; + + int index = getIndex(physical, incompleteCode); + myResolveMaps[index].put(ref, new SoftReference(results)); + } + } + + //for Visual Fabrique + public void clearResolveCaches(PsiReference ref) { + synchronized (PsiLock.LOCK) { + myClearCount++; + final boolean physical = ref.getElement().isPhysical(); + setCachedPolyVariantResolve(ref, null, physical, false, myClearCount); + setCachedPolyVariantResolve(ref, null, physical, true, myClearCount); + } + } + + private Reference getCachedResolve(PsiReference ref, boolean physical, boolean incompleteCode) { + synchronized (PsiLock.LOCK) { + int index = getIndex(physical, incompleteCode); + final Reference reference = (Reference)myResolveMaps[index].get(ref); + if(reference == null) return null; + return reference; + } + } + + public ResolveResult[] resolveWithCaching(PsiPolyVariantReference ref, + PolyVariantResolver resolver, + boolean needToPreventRecursion, + boolean incompleteCode) { + ProgressManager.getInstance().checkCanceled(); + + int clearCountOnStart; + synchronized (PsiLock.LOCK) { + clearCountOnStart = myClearCount; + } + + boolean physical = ref.getElement().isPhysical(); + final ResolveResult[] cached = getCachedPolyVariantResolve(ref, physical, incompleteCode); + if (cached != null) return cached; + + if (incompleteCode) { + final ResolveResult[] results = resolveWithCaching(ref, resolver, needToPreventRecursion, false); + if (results != null && results.length > 0) { + setCachedPolyVariantResolve(ref, results, physical, true, clearCountOnStart); + return results; + } + } + + if (!lockElement(ref, needToPreventRecursion)) return JavaResolveResult.EMPTY_ARRAY; + ResolveResult[] result; + try { + result = resolver.resolve(ref, incompleteCode); + } finally { + unlockElement(ref, needToPreventRecursion); + } + + setCachedPolyVariantResolve(ref, result, physical, incompleteCode, clearCountOnStart); + return result; + } + + private static int getIndex(boolean physical, boolean ic){ + return (physical ? 0 : 1) << 1 | (ic ? 1 : 0); + } + + private void setCachedPolyVariantResolve(PsiReference ref, ResolveResult[] result, boolean physical, boolean incomplete, int clearCountOnStart){ + synchronized (PsiLock.LOCK) { + if (clearCountOnStart != myClearCount && result != null) return; + int index = getIndex(physical, incomplete); + myPolyVariantResolveMaps[index].put(ref, new SoftReference(result)); + } + } + + private ResolveResult[] getCachedPolyVariantResolve(PsiReference ref, boolean physical, boolean ic){ + synchronized (PsiLock.LOCK) { + int index = getIndex(physical, ic); + final Reference reference = (Reference)myPolyVariantResolveMaps[index].get(ref); + if(reference == null) return null; + return reference.get(); + } + } + + public static interface ConstValueComputer{ + Object execute(PsiVariable variable, Set visitedVars); + } + + public Object computeConstantValueWithCaching(PsiVariable variable, ConstValueComputer computer, Set visitedVars){ + boolean physical = variable.isPhysical(); + + Object cached = (physical ? myVarToConstValueMap1 : myVarToConstValueMap2).get(variable); + if (cached == NULL) return null; + if (cached != null) return cached; + + Object result = computer.execute(variable, visitedVars); + + (physical ? myVarToConstValueMap1 : myVarToConstValueMap2).put(variable, result != null ? result : NULL); + + return result; + } + + public WeakHashMap getOrCreateWeakMap(final PsiManagerImpl manager, final Key> key, boolean forPhysical) { + MapPair pair = manager.getUserData(key); + if (pair == null){ + pair = new MapPair(); + manager.putUserData(key, pair); + + final MapPair _pair = pair; + manager.registerRunnableToRunOnChange( + new Runnable() { + public void run() { + synchronized (PsiLock.LOCK) { + myClearCount++; + _pair.physicalMap.clear(); + } + } + } + ); + manager.registerRunnableToRunOnAnyChange( + new Runnable() { + public void run() { + synchronized (PsiLock.LOCK) { + myClearCount++; + _pair.nonPhysicalMap.clear(); + } + } + } + ); + } + return forPhysical ? pair.physicalMap : pair.nonPhysicalMap; + } + + public static class MapPair{ + public WeakHashMap physicalMap; + public WeakHashMap nonPhysicalMap; + + public MapPair() { + physicalMap = new WeakHashMap(); + nonPhysicalMap = new WeakHashMap(); + } + } +} diff --git a/platform/platform-tests/testData/diff/applyPatch/matchByContext/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/matchByContext/after/1.txt new file mode 100644 index 000000000000..7aefef3ad5d6 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/matchByContext/after/1.txt @@ -0,0 +1,8 @@ +this +file +contains +a +number +of +modified +lines diff --git a/platform/platform-tests/testData/diff/applyPatch/matchByContext/apply.patch b/platform/platform-tests/testData/diff/applyPatch/matchByContext/apply.patch new file mode 100644 index 000000000000..27fabc9de5cc --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/matchByContext/apply.patch @@ -0,0 +1,10 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Thu Nov 16 15:03:12 2006 ++++ after/1.txt Thu Nov 16 15:03:15 2006 +@@ -3,5 +3,5 @@ + a + number + of +-unmodified ++modified + lines diff --git a/platform/platform-tests/testData/diff/applyPatch/matchByContext/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/matchByContext/before/1.txt new file mode 100644 index 000000000000..c4f4e573d39a --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/matchByContext/before/1.txt @@ -0,0 +1,8 @@ +this +file +contains +a +number +of +unmodified +lines diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/after/1.txt new file mode 100644 index 000000000000..902440ddd46b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/after/1.txt @@ -0,0 +1,8 @@ +thidss +file +contains +a +number +of +unmodified +linesds diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/apply.patch new file mode 100644 index 000000000000..9ee6d0a2bb97 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/apply.patch @@ -0,0 +1,15 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Thu Nov 16 15:03:12 2006 ++++ after/1.txt Thu Nov 16 15:03:15 2006 +@@ -1,8 +1,8 @@ +-thids ++thidss + file + contains + a + number + of + unmodified +-linesd +\ No newline at end of file ++linesds diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/before/1.txt new file mode 100644 index 000000000000..2a8aa3d549b9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileAddLastEmptyLine/before/1.txt @@ -0,0 +1,8 @@ +thids +file +contains +a +number +of +unmodified +linesd \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/after/1.txt new file mode 100644 index 000000000000..5424763980fb --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/after/1.txt @@ -0,0 +1,8 @@ +thiss +file +contains +a +number +of +unmodified +liness diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/apply.patch new file mode 100644 index 000000000000..925b4064e36d --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/apply.patch @@ -0,0 +1,14 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Thu Nov 16 15:03:12 2006 ++++ after/1.txt Thu Nov 16 15:03:15 2006 +@@ -1,8 +1,8 @@ +-this ++thiss + file + contains + a + number + of + unmodified +-lines ++liness diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/before/1.txt new file mode 100644 index 000000000000..c4f4e573d39a --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileKeepLastEmptyLine/before/1.txt @@ -0,0 +1,8 @@ +this +file +contains +a +number +of +unmodified +lines diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/after/1.txt new file mode 100644 index 000000000000..b6f4c34102fe --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/after/1.txt @@ -0,0 +1,8 @@ +thidss +file +contains +a +number +of +unmodified +linesds \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/apply.patch new file mode 100644 index 000000000000..16f739fe01e8 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/apply.patch @@ -0,0 +1,16 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Thu Nov 16 15:03:12 2006 ++++ after/1.txt Thu Nov 16 15:03:15 2006 +@@ -1,8 +1,8 @@ +-thids ++thidss + file + contains + a + number + of + unmodified +-linesd +\ No newline at end of file ++linesds +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/before/1.txt new file mode 100644 index 000000000000..2a8aa3d549b9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileLastLine/before/1.txt @@ -0,0 +1,8 @@ +thids +file +contains +a +number +of +unmodified +linesd \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/after/1.txt new file mode 100644 index 000000000000..5880be4fc3c9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/after/1.txt @@ -0,0 +1,19 @@ +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + @NotNull + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/apply.patch new file mode 100644 index 000000000000..7a0230f0193b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/apply.patch @@ -0,0 +1,20 @@ +diff --git a/before/1.txt b/after/1.txt +index fad3998..5880be4 100644 +--- a/before/1.txt ++++ b/after/1.txt +@@ -1,3 +1,5 @@ ++import org.jetbrains.annotations.NotNull; ++ + import java.util.function.Supplier; + class Test { + +@@ -6,6 +8,7 @@ class Test { + b(newMethod()); + } + ++ @NotNull + private Supplier newMethod() { + return (s) -> { + System.out.println(s); +-- +2.6.3.windows.1 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/before/1.txt new file mode 100644 index 000000000000..fad399888133 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileNoHunkAtEOF/before/1.txt @@ -0,0 +1,16 @@ +import java.util.function.Supplier; +class Test { + + private void a() + { + b(newMethod()); + } + + private Supplier newMethod() { + return (s) -> { + System.out.println(s); + }; + } + + void b(Supplier s) {} +} \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/after/1.txt new file mode 100644 index 000000000000..2a8aa3d549b9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/after/1.txt @@ -0,0 +1,8 @@ +thids +file +contains +a +number +of +unmodified +linesd \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/apply.patch new file mode 100644 index 000000000000..20ee0a63a7f7 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/apply.patch @@ -0,0 +1,15 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Thu Nov 16 15:03:12 2006 ++++ after/1.txt Thu Nov 16 15:03:15 2006 +@@ -1,8 +1,8 @@ +-this ++thids + file + contains + a + number + of + unmodified +-lines ++linesd +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/before/1.txt new file mode 100644 index 000000000000..c4f4e573d39a --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyFileRemoveLastEmptyLine/before/1.txt @@ -0,0 +1,8 @@ +this +file +contains +a +number +of +unmodified +lines diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLine/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyLine/after/1.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLine/after/1.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLine/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyLine/apply.patch new file mode 100644 index 000000000000..0089cee73d99 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLine/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/1.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLine/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyLine/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLine/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/after/1.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/after/1.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/apply.patch b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/apply.patch new file mode 100644 index 000000000000..d40114f6e14c --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/apply.patch @@ -0,0 +1,8 @@ +--- before/1.txt before ++++ after/1.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged +-- +1.9.7.5 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/modifyLineWithGitVersion/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/2.txt b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/2.txt new file mode 100644 index 000000000000..2f54c87dffb0 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/2.txt @@ -0,0 +1 @@ +conflict file \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/new/2.txt b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/new/2.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/after/new/2.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/apply.patch b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/apply.patch new file mode 100644 index 000000000000..1c160eca4a96 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/new/2.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/2.txt b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/2.txt new file mode 100644 index 000000000000..2f54c87dffb0 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveAndRenameWithNameConflicts/before/2.txt @@ -0,0 +1 @@ +conflict file \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFile/after/new/2.txt b/platform/platform-tests/testData/diff/applyPatch/moveFile/after/new/2.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFile/after/new/2.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/moveFile/apply.patch new file mode 100644 index 000000000000..1c160eca4a96 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFile/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/new/2.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFile/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/moveFile/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFile/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/after/new/1.txt b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/after/new/1.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/after/new/1.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/apply.patch b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/apply.patch new file mode 100644 index 000000000000..47a33d45a211 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/new/1.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/moveFileWithoutRename/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/multiFile/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/multiFile/after/1.txt new file mode 100644 index 000000000000..ff6e6b1a5055 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/multiFile/after/1.txt @@ -0,0 +1,3 @@ +first +second +third diff --git a/platform/platform-tests/testData/diff/applyPatch/multiFile/after/2.txt b/platform/platform-tests/testData/diff/applyPatch/multiFile/after/2.txt new file mode 100644 index 000000000000..15bf6080d09b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/multiFile/after/2.txt @@ -0,0 +1,3 @@ +uno +dos +tres diff --git a/platform/platform-tests/testData/diff/applyPatch/multiFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/multiFile/apply.patch new file mode 100644 index 000000000000..72c7370ec5f2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/multiFile/apply.patch @@ -0,0 +1,15 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Tue Nov 07 13:32:58 2006 ++++ after/1.txt Tue Nov 07 13:32:58 2006 +@@ -1,2 +1,3 @@ + first ++second + third +diff -r -u before/2.txt after/2.txt +--- before/2.txt Fri Nov 17 18:26:49 2006 ++++ after/2.txt Fri Nov 17 18:26:38 2006 +@@ -1,4 +1,3 @@ + uno + dos + tres +-cuatro diff --git a/platform/platform-tests/testData/diff/applyPatch/multiFile/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/multiFile/before/1.txt new file mode 100644 index 000000000000..1effb1028069 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/multiFile/before/1.txt @@ -0,0 +1,2 @@ +first +third diff --git a/platform/platform-tests/testData/diff/applyPatch/multiFile/before/2.txt b/platform/platform-tests/testData/diff/applyPatch/multiFile/before/2.txt new file mode 100644 index 000000000000..8c857923b895 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/multiFile/before/2.txt @@ -0,0 +1,4 @@ +uno +dos +tres +cuatro diff --git a/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/after/1.txt new file mode 100644 index 000000000000..3664b17e2df8 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/after/1.txt @@ -0,0 +1,3 @@ +uno +dos +tres \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/apply.patch b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/apply.patch new file mode 100644 index 000000000000..571c856741e8 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/apply.patch @@ -0,0 +1,10 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Fri Nov 24 13:06:16 2006 ++++ after/1.txt Fri Nov 24 13:06:20 2006 +@@ -1,2 +1,3 @@ + uno +-dos +\ No newline at end of file ++dos ++tres +\ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/before/1.txt new file mode 100644 index 000000000000..8306d505b4d9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/noNewlineAtEof/before/1.txt @@ -0,0 +1,2 @@ +uno +dos \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/after/new.txt b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/after/new.txt new file mode 100644 index 000000000000..7f0619187c33 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/after/new.txt @@ -0,0 +1 @@ +new text \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/apply.patch b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/apply.patch new file mode 100644 index 000000000000..c98f10b5b280 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/apply.patch @@ -0,0 +1,5 @@ +--- after/new.txt 1970-01-01 01:00:00.000000000 +0100 ++++ after/new.txt 2007-02-08 12:36:21.296875000 +0100 +@@ -0,0 +1 @@ ++new text +\ No newline at end of file \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/omittedChunkSize/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/overlappingContext/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/after/1.txt new file mode 100644 index 000000000000..b2f931a67315 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/after/1.txt @@ -0,0 +1,5 @@ +one +two +three +four +five diff --git a/platform/platform-tests/testData/diff/applyPatch/overlappingContext/apply.patch b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/apply.patch new file mode 100644 index 000000000000..8a27afce33d3 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/apply.patch @@ -0,0 +1,8 @@ +--- before/1.txt before ++++ after/1.txt after +@@ -1,3 +1,5 @@ + one ++two + three ++four + five diff --git a/platform/platform-tests/testData/diff/applyPatch/overlappingContext/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/before/1.txt new file mode 100644 index 000000000000..a3e0a3b57922 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/overlappingContext/before/1.txt @@ -0,0 +1,3 @@ +one +three +five diff --git a/platform/platform-tests/testData/diff/applyPatch/partialApply/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/partialApply/after/1.txt new file mode 100644 index 000000000000..1978663eef3b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/partialApply/after/1.txt @@ -0,0 +1,7 @@ +uno +dos +cuatro +cinco +seis +siete +ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/partialApply/apply.patch b/platform/platform-tests/testData/diff/applyPatch/partialApply/apply.patch new file mode 100644 index 000000000000..a735d9f809f9 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/partialApply/apply.patch @@ -0,0 +1,13 @@ +diff -r -u before/1.txt after/1.txt +--- before/1.txt Tue Nov 21 14:20:19 2006 ++++ after/1.txt Tue Nov 21 14:20:15 2006 +@@ -1,7 +1,7 @@ + uno + dos +-tres + cuatro ++cinco + seis + siete +-octo ++ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/partialApply/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/partialApply/before/1.txt new file mode 100644 index 000000000000..f43492596a39 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/partialApply/before/1.txt @@ -0,0 +1,8 @@ +uno +dos +tres +cuatro +cinco +seis +siete +ocho diff --git a/platform/platform-tests/testData/diff/applyPatch/removeFile/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/removeFile/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/removeFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/removeFile/apply.patch new file mode 100644 index 000000000000..b3454e0cbd66 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/removeFile/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ before/1.txt before +@@ -1,3 +0,0 @@ +-One +-Two +-Three diff --git a/platform/platform-tests/testData/diff/applyPatch/removeFile/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/removeFile/before/1.txt new file mode 100644 index 000000000000..4fcefbf2acb2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/removeFile/before/1.txt @@ -0,0 +1,3 @@ +One +Two +Three diff --git a/platform/platform-tests/testData/diff/applyPatch/removeFile/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/removeFile/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/1.txt b/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/1.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/1.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/2.txt b/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/2.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameDir/after/new/2.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/after/old/empty b/platform/platform-tests/testData/diff/applyPatch/renameDir/after/old/empty new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/apply.patch b/platform/platform-tests/testData/diff/applyPatch/renameDir/apply.patch new file mode 100644 index 000000000000..11ece787e04d --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameDir/apply.patch @@ -0,0 +1,12 @@ +--- before/old/1.txt before ++++ after/new/1.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged +--- before/old/2.txt before ++++ after/new/2.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/1.txt b/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/2.txt b/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/2.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameDir/before/old/2.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameFile/after/2.txt b/platform/platform-tests/testData/diff/applyPatch/renameFile/after/2.txt new file mode 100644 index 000000000000..872d75fb1837 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameFile/after/2.txt @@ -0,0 +1,2 @@ +new +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameFile/apply.patch b/platform/platform-tests/testData/diff/applyPatch/renameFile/apply.patch new file mode 100644 index 000000000000..45f04f13a9d2 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameFile/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ after/2.txt after +@@ -1,2 +1,2 @@ additional context +-old ++new + unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/renameFile/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/renameFile/before/1.txt new file mode 100644 index 000000000000..0560c1782c61 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/renameFile/before/1.txt @@ -0,0 +1,2 @@ +old +unchanged diff --git a/platform/platform-tests/testData/diff/applyPatch/reversedNames/after/1.txt b/platform/platform-tests/testData/diff/applyPatch/reversedNames/after/1.txt new file mode 100644 index 000000000000..23c83ff62c2e --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/reversedNames/after/1.txt @@ -0,0 +1,5 @@ +uno +dos +tres +cuatro +cinco diff --git a/platform/platform-tests/testData/diff/applyPatch/reversedNames/apply.patch b/platform/platform-tests/testData/diff/applyPatch/reversedNames/apply.patch new file mode 100644 index 000000000000..1784f7646a6b --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/reversedNames/apply.patch @@ -0,0 +1,8 @@ +diff -r -u before/1.txt after/1.txt +--- 1.txt.old Mon Nov 20 18:17:02 2006 ++++ 1.txt Mon Nov 20 18:17:18 2006 +@@ -2,3 +2,4 @@ + dos + tres + cuatro ++cinco diff --git a/platform/platform-tests/testData/diff/applyPatch/reversedNames/before/1.txt.old b/platform/platform-tests/testData/diff/applyPatch/reversedNames/before/1.txt.old new file mode 100644 index 000000000000..8c857923b895 --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/reversedNames/before/1.txt.old @@ -0,0 +1,4 @@ +uno +dos +tres +cuatro diff --git a/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/after/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/after/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/apply.patch b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/apply.patch new file mode 100644 index 000000000000..4a7604b6c56d --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/apply.patch @@ -0,0 +1,6 @@ +--- before/1.txt before ++++ before/1.txt before +@@ -1,3 +0,0 @@ +-One +--- Two +-Three diff --git a/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/before/1.txt b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/before/1.txt new file mode 100644 index 000000000000..aed625337e9f --- /dev/null +++ b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/before/1.txt @@ -0,0 +1,3 @@ +One +-- Two +Three diff --git a/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/before/unchanged.txt b/platform/platform-tests/testData/diff/applyPatch/wrongFileStartUnified/before/unchanged.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/binaryPatch/addedEmptyPng/data.bin b/platform/platform-tests/testData/diff/binaryPatch/addedEmptyPng/data.bin new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/binaryPatch/addedEmptyPng/file.patch b/platform/platform-tests/testData/diff/binaryPatch/addedEmptyPng/file.patch new file mode 100644 index 000000000000..025f448b8bfd --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/addedEmptyPng/file.patch @@ -0,0 +1,7 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +GIT binary patch +literal 0 +Hc$@z_u719puq!NScSPpF +zg#4pv1xM2>&lOf*D6TwP`0>{VkG0l*8=OKmdxUNAj@;&(aUeGLNb=vme}PsrPzDr# +zvM@3*Xff!3I3PbUuoXE>E%4Bh5}neqWRn4xyvD>5E+G-wWh=v$Y~(Y)t?BtiGU`x} +zvwN4I+p^^=6}O6owiIS+*!e$IbV>-)ROHmu6b??+5(wsL?}%b`;z;W6j1qTZmQ0e1 +hmT{3#&QMb2b7h$uEzRr4%&s6UEidM`-OZ7~8UVBne@g%W + diff --git a/platform/platform-tests/testData/diff/binaryPatch/addedPng/data.bin b/platform/platform-tests/testData/diff/binaryPatch/addedPng/data.bin new file mode 100644 index 000000000000..12a40d7b1aa4 Binary files /dev/null and b/platform/platform-tests/testData/diff/binaryPatch/addedPng/data.bin differ diff --git a/platform/platform-tests/testData/diff/binaryPatch/addedPng/file.patch b/platform/platform-tests/testData/diff/binaryPatch/addedPng/file.patch new file mode 100644 index 000000000000..088541f0c283 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/addedPng/file.patch @@ -0,0 +1,15 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..12a40d7b1aa4ddfbd6ae09916e76dad11de9f181 +GIT binary patch +literal 437 +zc$@*X0ZRUfP)IXy?-;=MPMqTz?Ll58Xr(!10oMG34gQ~63H$lcsYT|k=CKBP`%YT4%2Z$GeXwDjawBI +zYO`KZpR=$mbdNwYLK_QcM%+XdY@Sb#iHqo{N*wt!ni1Mq;0(J5Rp0{oQg}pEfJhlkh_P00000NkvXXu0mjf{6@cZ + diff --git a/platform/platform-tests/testData/diff/binaryPatch/len1/data.bin b/platform/platform-tests/testData/diff/binaryPatch/len1/data.bin new file mode 100644 index 000000000000..ef6bce1d1d15 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len1/data.bin @@ -0,0 +1 @@ +M \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/binaryPatch/len1/file.patch b/platform/platform-tests/testData/diff/binaryPatch/len1/file.patch new file mode 100644 index 000000000000..582fedb102fb --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len1/file.patch @@ -0,0 +1,7 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..ef6bce1d1d15c6721aa1c5cce64b10378dfcc844 +GIT binary patch +literal 1 +Ic%1VE002$^P5=M^ + diff --git a/platform/platform-tests/testData/diff/binaryPatch/len2/data.bin b/platform/platform-tests/testData/diff/binaryPatch/len2/data.bin new file mode 100644 index 000000000000..e290d5c57c03 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len2/data.bin @@ -0,0 +1 @@ +Ma \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/binaryPatch/len2/file.patch b/platform/platform-tests/testData/diff/binaryPatch/len2/file.patch new file mode 100644 index 000000000000..f96a404fc2c3 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len2/file.patch @@ -0,0 +1,7 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..e290d5c57c0342a157682020cf4576467dcfcf11 +GIT binary patch +literal 2 +Jc%1W11ONd20IvW5 + diff --git a/platform/platform-tests/testData/diff/binaryPatch/len3/data.bin b/platform/platform-tests/testData/diff/binaryPatch/len3/data.bin new file mode 100644 index 000000000000..233c4e17cb33 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len3/data.bin @@ -0,0 +1 @@ +Man \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/binaryPatch/len3/file.patch b/platform/platform-tests/testData/diff/binaryPatch/len3/file.patch new file mode 100644 index 000000000000..2d8ed9f5f0cb --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/len3/file.patch @@ -0,0 +1,7 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..233c4e17cb33cd5ff0a6ca32c6c9cdafae4f670d +GIT binary patch +literal 3 +Kc%1W1%mV-d8UY;u + diff --git a/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/data.bin b/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/data.bin new file mode 100644 index 000000000000..9fec674bf042 --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/data.bin @@ -0,0 +1 @@ +Man is distingui \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/file.patch b/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/file.patch new file mode 100644 index 000000000000..b7bc15f0558b --- /dev/null +++ b/platform/platform-tests/testData/diff/binaryPatch/letterXasLen/file.patch @@ -0,0 +1,7 @@ +diff --git data.bin data.bin +new file mode 100644 +index 0000000000000000000000000000000000000000..9fec674bf042a5e2b618c28c3753e2a94e9d16ca +GIT binary patch +literal 16 +Xc%1W1%u~oLR!GS#F3HSGFU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/platform-tests/testData/diff/trim/formattingOnly.1 b/platform/platform-tests/testData/diff/trim/formattingOnly.1 new file mode 100644 index 000000000000..5030b60c7e1f --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/formattingOnly.1 @@ -0,0 +1,4 @@ +start +change + a + b \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/trim/formattingOnly.2 b/platform/platform-tests/testData/diff/trim/formattingOnly.2 new file mode 100644 index 000000000000..3a44a789fc81 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/formattingOnly.2 @@ -0,0 +1,4 @@ +start +CHANGE + a + b \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/trim/formattingOnly.lines1 b/platform/platform-tests/testData/diff/trim/formattingOnly.lines1 new file mode 100644 index 000000000000..eb1b1446eafd --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/formattingOnly.lines1 @@ -0,0 +1,3 @@ +LT 1 +C 6-13 +LB 1 diff --git a/platform/platform-tests/testData/diff/trim/formattingOnly.lines2 b/platform/platform-tests/testData/diff/trim/formattingOnly.lines2 new file mode 100644 index 000000000000..eb1b1446eafd --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/formattingOnly.lines2 @@ -0,0 +1,3 @@ +LT 1 +C 6-13 +LB 1 diff --git a/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.1 b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.1 new file mode 100644 index 000000000000..5c1dd17c6a53 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.1 @@ -0,0 +1 @@ +spacesAtStart diff --git a/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.2 b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.2 new file mode 100644 index 000000000000..4170add7e889 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.2 @@ -0,0 +1 @@ + spacesAtStart diff --git a/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.lines1 b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.lines1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.lines2 b/platform/platform-tests/testData/diff/trim/noSpaceOnOneSide.lines2 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testData/diff/trim/trimBug1.1 b/platform/platform-tests/testData/diff/trim/trimBug1.1 new file mode 100644 index 000000000000..560934fa9597 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug1.1 @@ -0,0 +1,3 @@ + } + return new DiffFragment(notEmptyContent(buffer1), notEmptyContent(buffer2)); + } diff --git a/platform/platform-tests/testData/diff/trim/trimBug1.2 b/platform/platform-tests/testData/diff/trim/trimBug1.2 new file mode 100644 index 000000000000..6cbd6777a200 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug1.2 @@ -0,0 +1,5 @@ + } + String text1 = notEmptyContent(buffer1); + String text2 = notEmptyContent(buffer2); + return isEqual ? DiffFragment.unchanged(text1, text2) : new DiffFragment(text1, text2); + } diff --git a/platform/platform-tests/testData/diff/trim/trimBug1.lines1 b/platform/platform-tests/testData/diff/trim/trimBug1.lines1 new file mode 100644 index 000000000000..7ec67b5aaf52 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug1.lines1 @@ -0,0 +1,6 @@ +LT 1 +I 83-83 +C 10-34 +C 58-59 +D 84-85 +LB 1 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/trim/trimBug1.lines2 b/platform/platform-tests/testData/diff/trim/trimBug1.lines2 new file mode 100644 index 000000000000..05ffec472433 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug1.lines2 @@ -0,0 +1,6 @@ +C 10-25 +C 49-69 +D 186-186 +I 93-185 +LT 1 +LB 3 diff --git a/platform/platform-tests/testData/diff/trim/trimBug2.1 b/platform/platform-tests/testData/diff/trim/trimBug2.1 new file mode 100644 index 000000000000..9657a6891641 --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug2.1 @@ -0,0 +1,4 @@ + private final DiffStatusBar myStatusBar; + private final DiffToolbarComponent myToolbar; + private final DiffToolbar myDefaultActions; + private DataProvider myDataProvider = null; diff --git a/platform/platform-tests/testData/diff/trim/trimBug2.2 b/platform/platform-tests/testData/diff/trim/trimBug2.2 new file mode 100644 index 000000000000..994e9b0b16ff --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug2.2 @@ -0,0 +1,4 @@ + private final DiffStatusBar myStatusBar; + private final DiffToolbarComponent myToolbar; + private final DiffRequest.ToolbarAddons myDefaultActions; + private DataProvider myDataProvider = null; diff --git a/platform/platform-tests/testData/diff/trim/trimBug2.lines1 b/platform/platform-tests/testData/diff/trim/trimBug2.lines1 new file mode 100644 index 000000000000..049f3f0a403f --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug2.lines1 @@ -0,0 +1,8 @@ +LT 1 +LT 1 +D 79-89 +LB 1 +LT 2 +C 117-138 +LB 2 +LB 2 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/trim/trimBug2.lines2 b/platform/platform-tests/testData/diff/trim/trimBug2.lines2 new file mode 100644 index 000000000000..3dc57825b72f --- /dev/null +++ b/platform/platform-tests/testData/diff/trim/trimBug2.lines2 @@ -0,0 +1,8 @@ +LT 1 +LT 1 +D 79-79 +LB 1 +LT 2 +C 107-132 +LB 2 +LB 2 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/ver1.txt b/platform/platform-tests/testData/diff/ver1.txt new file mode 100644 index 000000000000..53c7e94bff20 --- /dev/null +++ b/platform/platform-tests/testData/diff/ver1.txt @@ -0,0 +1,14 @@ +ABC +bvcbcvbcvb +vcb +---- +---- +leftOnly +---- +123 +---- +abc +___ +a +b +3 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/ver2.txt b/platform/platform-tests/testData/diff/ver2.txt new file mode 100644 index 000000000000..e507beae0c42 --- /dev/null +++ b/platform/platform-tests/testData/diff/ver2.txt @@ -0,0 +1,15 @@ +abc +---- +xyz +---- +leftOnly +rightOnly +---- +123 +---- +456 +789 +___ +1 +2 +3 \ No newline at end of file diff --git a/platform/platform-tests/testData/diff/ver3.txt b/platform/platform-tests/testData/diff/ver3.txt new file mode 100644 index 000000000000..f77289715158 --- /dev/null +++ b/platform/platform-tests/testData/diff/ver3.txt @@ -0,0 +1,12 @@ +---- +---- +rightOnly +---- +!!! +---- +456 +789 +___ +1 +a +b \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/BaseDiffTestCase.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/BaseDiffTestCase.java new file mode 100644 index 000000000000..8f5883790799 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/BaseDiffTestCase.java @@ -0,0 +1,130 @@ +/* + * Copyright 2000-2017 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.diff; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.diff.impl.ComparisonPolicy; +import com.intellij.openapi.diff.impl.DiffPanelImpl; +import com.intellij.openapi.diff.impl.external.DiffManagerImpl; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.diff.impl.processing.HighlightMode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; + +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public abstract class BaseDiffTestCase extends PlatformTestCase { + private File myFile1; + private File myFile2; + public static final DiffContent.Listener SHOULD_NOT_INVALIDATE = new DiffContent.Listener() { + @Override + public void contentInvalid() { + fail(); + } + }; + + public static String readFile(File file) throws IOException { + FileInputStream stream = new FileInputStream(file); + byte[] bytes = new byte[(int) file.length()]; + stream.read(bytes); + return LineTokenizer.correctLineSeparators(new String(bytes)); + } + + protected DiffPanelImpl createDiffPanel(Window ownerWindow, Project project, boolean enableToolbar) { + DiffPanelImpl diffPanel = new DiffPanelImpl(ownerWindow, project, enableToolbar, true, + DiffManagerImpl.FULL_DIFF_DIVIDER_POLYGONS_OFFSET, null); + Disposer.register(project, diffPanel); + return diffPanel; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + DiffManagerImpl.getInstanceEx().setComparisonPolicy(ComparisonPolicy.DEFAULT); + DiffManagerImpl.getInstanceEx().setHighlightMode(HighlightMode.BY_WORD); + } + + @Override + protected void tearDown() throws Exception { + myFile1 = null; + myFile2 = null; + super.tearDown(); + } + + public static File getFile(String name) { + return new File(getDirectory(), name); + } + + public static File getDirectory() { + String testDataRoot = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/"; + return new File(testDataRoot, "diff"); + } + + protected void setFile1(String file1) { myFile1 = getFile(file1); } + + protected void setFile2(String file2) { myFile2 = getFile(file2); } + + protected DiffPanelImpl loadFiles() throws IOException { + DiffPanelImpl diffPanel = createDiffPanel(null, myProject, false); + String content1 = content1(); + String content2 = content2(); + setContents(diffPanel, content1, content2); + return diffPanel; + } + + protected String content2() throws IOException { + return readFile(myFile2); + } + + protected String content1() throws IOException { + return readFile(myFile1); + } + + protected void checkTextEqual(String content, Editor editor) { + assertEquals(content.replaceAll("\r\n", "\n"), editor.getDocument().getText()); + } + + protected void setContents(final DiffPanelImpl diffPanel, final String content1, final String content2) { + diffPanel.setContents(new SimpleContent(content1), new SimpleContent(content2)); + } + + protected static Editor getEditor2(DiffPanelImpl diffPanel) { + return diffPanel.getEditor(FragmentSide.SIDE2); + } + + protected static Editor getEditor1(DiffPanelImpl diffPanel) { + return diffPanel.getEditor(FragmentSide.SIDE1); + } + + protected void replaceString(final Document document, final int startOffset, final int endOffset, final String string) { + replaceString2(myProject, document, startOffset, endOffset, string); + } + + public static void replaceString2(final Project project, final Document document, + final int startOffset, + final int endOffset, + final String string) { + ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(project, () -> document.replaceString(startOffset, endOffset, string), null, null)); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/LineTokenizerTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/LineTokenizerTest.java index 7cf0ce20b690..b5cbd22339bb 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/LineTokenizerTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/LineTokenizerTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff; import com.intellij.util.Assertion; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/SimpleContentTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/SimpleContentTest.java new file mode 100644 index 000000000000..2cc232d94534 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/SimpleContentTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2017 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.diff; + +import com.intellij.openapi.editor.Document; +import com.intellij.testFramework.PlatformTestCase; + +import java.io.IOException; +import java.util.Arrays; + +public class SimpleContentTest extends PlatformTestCase { + public void testEqualsAndDifferent() throws IOException { + SimpleContent content1 = new SimpleContent("a\nb"); + SimpleContent content2 = new SimpleContent("a\nb"); + assertTrue(Arrays.equals(content1.getBytes(), content2.getBytes())); + assertEquals(content1.getText(), content2.getText()); + assertEquals(content1.getDocument().getText(), content2.getDocument().getText()); + + content1 = new SimpleContent("a\nb"); + content2 = new SimpleContent("a\r\nb"); + assertFalse(Arrays.equals(content1.getBytes(), content2.getBytes())); + assertFalse(content1.getText().equals(content2.getText())); + assertTrue(content1.getDocument().getText().equals(content2.getDocument().getText())); + + content1 = new SimpleContent("a\nb"); + content2 = new SimpleContent("a\nc"); + assertFalse(Arrays.equals(content1.getBytes(), content2.getBytes())); + assertFalse(content1.getText().equals(content2.getText())); + assertFalse(content1.getDocument().getText().equals(content2.getDocument().getText())); + + SimpleContent content = new SimpleContent("a\nb\r\nc"); + assertTrue(Arrays.equals("a\nb\r\nc".getBytes(), content.getBytes())); + } + + public void testModifyContent() throws IOException { + SimpleContent content = new SimpleContent("abc\r\ndef"); + content.setReadOnly(false); + String originalText = content.getText(); + byte[] originalBytes = content.getBytes(); + Document document = content.getDocument(); + BaseDiffTestCase.replaceString2(myProject, document, 0, 3, "123"); + + String newText = "123\r\ndef"; + assertEquals(newText, content.getText()); + assertEquals("123\ndef", content.getDocument().getText()); + assertTrue(Arrays.equals(newText.getBytes(), content.getBytes())); + + BaseDiffTestCase.replaceString2(myProject, document, 0, 3, "abc"); + assertEquals("abc\ndef", document.getText()); + assertEquals(originalText, content.getText()); + assertEquals(originalBytes, content.getBytes()); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/actions/MergeOperationsTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/actions/MergeOperationsTest.java new file mode 100644 index 000000000000..38699128801b --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/actions/MergeOperationsTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2000-2017 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.diff.actions; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.diff.impl.DiffPanelImpl; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.util.Assertion; + +import java.util.List; + +public class MergeOperationsTest extends BaseDiffTestCase { + private final Assertion CHECK = new Assertion(); + private DiffPanelImpl myDiffPanel; + private MergeOperations myMergeOperations1; + private SimpleContent myContent1; + private SimpleContent myContent2; + private MergeOperations myMergeOperations2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myDiffPanel = createDiffPanel(null, myProject, true); + myContent1 = new SimpleContent("abc\n123\nxyz"); + myContent1.setReadOnly(false); + myContent2 = new SimpleContent("abc\n098\nxyz"); + myContent2.setReadOnly(false); + myMergeOperations1 = new MergeOperations(myDiffPanel, FragmentSide.SIDE1); + myMergeOperations2 = new MergeOperations(myDiffPanel, FragmentSide.SIDE2); + setContents(); + } + + @Override + protected void tearDown() throws Exception { + myContent1 = null; + myContent2 = null; + myMergeOperations1 = null; + myMergeOperations2 = null; + myDiffPanel = null; + super.tearDown(); + } + + private void setContents() { + myDiffPanel.setContents(myContent1, myContent2); + } + + public void testNothingToDo() { + moveToOffset1(0); + CHECK.empty(myMergeOperations1.getOperations()); + } + + public void testInsert() { + moveToOffset1(6); + List operations = myMergeOperations1.getOperations(); + assertEquals(3, operations.size()); + MergeOperations.Operation insert = getRemove(operations); + insert.perform(myProject); + assertEquals("abc\nxyz", myContent1.getText()); + moveToOffset2(5); + myDiffPanel.getDiffUpdater().updateNow(); + operations = myMergeOperations2.getOperations(); + CHECK.size(2, operations); + } + + public void testReplace() { + moveToOffset1(6); + MergeOperations.Operation replace = getReplace(myMergeOperations1.getOperations()); + replace.perform(myProject); + assertEquals("abc\n123\nxyz", myContent2.getText()); + } + + public void testNoActionsBeforeRediff() { + assertEquals(1, countAction(getEditor1(myDiffPanel))); + assertEquals(1, countAction(getEditor2(myDiffPanel))); + replaceString(myContent1.getDocument(), 4, 6, "n\ne\nw"); + assertEquals(0, myDiffPanel.getLineBlocks().getCount()); + assertEquals(0, countAction(getEditor1(myDiffPanel))); + assertEquals(0, countAction(getEditor2(myDiffPanel))); + } + + private static int countAction(Editor editor) { + RangeHighlighter[] allHighlighters = editor.getMarkupModel().getAllHighlighters(); + int counter = 0; + for (RangeHighlighter highlighter : allHighlighters) { + GutterIconRenderer iconRenderer = (GutterIconRenderer)highlighter.getGutterIconRenderer(); + if (iconRenderer == null) continue; + AnAction action = iconRenderer.getClickAction(); + assertEquals(action == null, iconRenderer.getIcon() == null); + if (action != null) counter++; + } + return counter; + } + + private static MergeOperations.Operation getReplace(List operations) { + return findOperation(operations, "Replace"); + } + + private static MergeOperations.Operation getRemove(List operations) { + return findOperation(operations, "Remove"); + } + + private static MergeOperations.Operation findOperation(List operations, String name) { + for (MergeOperations.Operation operation : operations) { + if (operation.getName().contains(name)) { + return operation; + } + } + return null; + } + + private void moveToOffset1(int offset) { + getEditor1(myDiffPanel).getCaretModel().moveToOffset(offset); + } + + private void moveToOffset2(int offset) { + getEditor2(myDiffPanel).getCaretModel().moveToOffset(offset); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/DiffFilesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/DiffFilesTest.java new file mode 100644 index 000000000000..40f219ef092d --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/DiffFilesTest.java @@ -0,0 +1,275 @@ +/* + * Copyright 2000-2017 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.diff.impl; + +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.diff.DiffColors; +import com.intellij.openapi.diff.impl.util.TextDiffType; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.editor.impl.EditorImpl; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.rt.execution.junit.FileComparisonFailure; +import com.intellij.util.containers.HashMap; +import junit.framework.Test; +import junit.framework.TestSuite; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +public class DiffFilesTest extends TestSuite { + private static final String RESULT1_EXT = ".lines1"; + + public DiffFilesTest() { + File allTestsDir = BaseDiffTestCase.getDirectory(); + HashMap policies = MyIdeaTestCase.ourPolicyToDirectory; + for (ComparisonPolicy comparisonPolicy : policies.keySet()) { + File dir = new File(allTestsDir, policies.get(comparisonPolicy)); + if (!dir.exists()) continue; + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + String nameWithExt = file.getName(); + if (StringUtil.startsWithChar(nameWithExt, '_')) continue; + if (!nameWithExt.endsWith(RESULT1_EXT)) continue; + String name = nameWithExt.substring(0, nameWithExt.length() - RESULT1_EXT.length()); + addTest(new MyIdeaTestCase(name, comparisonPolicy) { + }); + } + } + } + + public static Test suite() { + return new DiffFilesTest(); + } + + public abstract static class MyIdeaTestCase extends BaseDiffTestCase { + private static final HashMap ourPolicyToDirectory = new HashMap<>(); + private File myResultFile1; + private File myResultFile2; + private String myName; + private final ComparisonPolicy myTestingPolicy; + + public MyIdeaTestCase(String name, ComparisonPolicy policy) { + this(name + ".1", name + ".2", name + RESULT1_EXT, name + ".lines2", policy); + myName = name; + } + + private MyIdeaTestCase(String file1, String file2, String result1, String result2, ComparisonPolicy policy) { + myTestingPolicy = policy; + String directory = ourPolicyToDirectory.get(myTestingPolicy) + File.separatorChar; + setFile1(directory + file1); + setFile2(directory + file2); + myResultFile1 = getTestFile(result1); + myResultFile2 = getTestFile(result2); + setName("test"); + } + + private File getTestFile(String name) { + return new File(getFile(ourPolicyToDirectory.get(myTestingPolicy)), name); + } + + @Override + public String getName() { + return myName != null ? myName : super.getName(); + } + + @Override + protected void resetAllFields() { + // Do nothing otherwise myName will be nulled out before getName() is called. + } + + public void test() throws IOException { + DiffPanelImpl diffPanel = createDiffPanel(null, myProject, false); + diffPanel.setComparisonPolicy(myTestingPolicy); + String content1 = content1(); + String content2 = content2(); + setContents(diffPanel, content1, content2); + Editor editor1 = BaseDiffTestCase.getEditor1(diffPanel); + checkResult("Editor 1", myResultFile1, editor1); + Editor editor2 = BaseDiffTestCase.getEditor2(diffPanel); + checkResult("Editor 2", myResultFile2, editor2); + checkTextEqual(content1, editor1); + checkTextEqual(content2, editor2); + } + + private void checkResult(String message, File resultFile, Editor editor1) throws IOException { + String expected = readFile(resultFile); + String actual = process(editor1); + List exp = StringUtil.split(expected, "\n"); + Collections.sort(exp); + List act = StringUtil.split(actual, "\n"); + Collections.sort(act); + if (!exp.equals(act)) { + throw new FileComparisonFailure(message, StringUtil.join(act, "\n"), StringUtil.join(act, "\n"), resultFile.getAbsolutePath()); + } + } + + protected String process(Editor editor) { + RangeHighlighter[] highlighters1 = editor.getMarkupModel().getAllHighlighters(); + StringBuffer result = new StringBuffer(); + for (RangeHighlighter highlighter : highlighters1) { + HighlighterTargetArea targetArea = highlighter.getTargetArea(); + if (targetArea == HighlighterTargetArea.EXACT_RANGE) { + processRange(highlighter, result, editor); + } + else if (targetArea == HighlighterTargetArea.LINES_IN_RANGE) { + if (highlighter.getLineSeparatorPlacement() == null && highlighter.getLayer() == CurrentLineMarker.LAYER) { + continue; + } + processLine(highlighter, result, editor); + } + else { + fail("Unknown highlighter: " + String.valueOf(targetArea)); + } + result.append('\n'); + } + return result.toString(); + } + + private static void processLine(RangeHighlighter highlighter, StringBuffer result, Editor editor) { + SeparatorPlacement placement = highlighter.getLineSeparatorPlacement(); + if (highlighter.getLineSeparatorColor() != null) { + result.append("L"); + } + else { + result.append("N"); + } + if (SeparatorPlacement.TOP.equals(placement)) { + result.append('T'); + } + else if (SeparatorPlacement.BOTTOM.equals(placement)) { + result.append('B'); + } + else { + fail("Unknown placement: " + String.valueOf(placement) + "(" + result + ")"); + } + int line = editor.getDocument().getLineNumber(highlighter.getStartOffset()); + result.append(' '); + result.append(line); + } + + private static void processRange(RangeHighlighter highlighter, StringBuffer result, Editor editor) { + int startOffset = highlighter.getStartOffset(); + int endOffset = highlighter.getEndOffset(); + if (startOffset == endOffset) { + assertEquals(EffectType.BOXED, highlighter.getTextAttributes().getEffectType()); + } + else { + assertTrue(startOffset + "<" + endOffset, startOffset < endOffset); + } + TextAttributes textAttributes = highlighter.getTextAttributes(); + Color originalColorOfInlineWrapper = getOriginalColor(textAttributes, editor); + + if (isAttributesKey(textAttributes, DiffColors.DIFF_INSERTED, editor)) { + result.append("I"); + appendOffsets(result, startOffset, endOffset); + } + else if (isAttributesKey(textAttributes, DiffColors.DIFF_DELETED, editor)) { + result.append("D"); + appendOffsets(result, startOffset, endOffset); + } + else if (isAttributesKey(textAttributes, DiffColors.DIFF_MODIFIED, editor)) { + result.append("C"); + appendOffsets(result, startOffset, endOffset); + } + else if (colorsEqual(getBgColor(DiffColors.DIFF_INSERTED, editor), originalColorOfInlineWrapper) || + colorsEqual(getBgColor(DiffColors.DIFF_DELETED, editor), originalColorOfInlineWrapper) || + colorsEqual(getBgColor(DiffColors.DIFF_MODIFIED, editor), originalColorOfInlineWrapper)) { + // ignoring inline wrapper highlighters + } + else if (textAttributes.getEffectType() == EffectType.BOXED) { + result.append("B"); + appendOffsets(result, startOffset, endOffset); + } + else { + fail(textAttributes.toString()); + } + } + + private static void appendOffsets(StringBuffer result, int startOffset, int endOffset) { + result.append(' '); + result.append(startOffset + "-" + endOffset); + } + + private static Color getBgColor(TextAttributesKey key, Editor editor) { + EditorColorsScheme colorsScheme = editor.getColorsScheme(); + return colorsScheme.getAttributes(key).getBackgroundColor(); + } + + private static boolean isAttributesKey(TextAttributes textAttributes, TextAttributesKey key, Editor editor) { + EditorColorsScheme colorsScheme = editor.getColorsScheme(); + return textAttributes.equals(colorsScheme.getAttributes(key)); + } + + static { + ourPolicyToDirectory.put(ComparisonPolicy.DEFAULT, "default"); + ourPolicyToDirectory.put(ComparisonPolicy.TRIM_SPACE, "trim"); + ourPolicyToDirectory.put(ComparisonPolicy.IGNORE_SPACE, "noSpaces"); + } + } + + /** + * The reverse of the formula in {@link TextDiffType#getMiddleColor(java.awt.Color, java.awt.Color, double)} to let the test + * identify and ignore highlighters of inline diff wrappers. + */ + @Nullable + private static Color getOriginalColor(@Nullable TextAttributes highlighterAttrs, @NotNull Editor editor) { + if (highlighterAttrs == null) { + return null; + } + + try { + Color fg = highlighterAttrs.getBackgroundColor(); + Color bg = ((EditorImpl)editor).getBackgroundColor(); + + int b = reverseMiddle(fg.getBlue(), bg.getBlue()); + int g = reverseMiddle(fg.getGreen(), bg.getGreen()); + int r = reverseMiddle(fg.getRed(), bg.getRed()); + + return new Color(r, g, b); + } + catch (IllegalArgumentException e) { + // some other color (not produced by the "get-middle-color" formula) when provided to the reverse formula + // may produce invalid values (more than 255). + return null; + } + } + + // reverseMiddle calculation may be not accurate enough => allow eps = 1 + private static boolean colorsEqual(@NotNull Color color1, @Nullable Color color2) { + if (color2 == null) { + return false; + } + + int eps = 1; + return Math.abs(color1.getRed() - color2.getRed()) <= eps && + Math.abs(color1.getGreen() - color2.getGreen()) <= eps && + Math.abs(color1.getBlue() - color2.getBlue()) <= eps; + } + + private static int reverseMiddle(int fb, int bb) { + return (5*fb - 3*bb) / 2; + } + +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/IgnoreWhiteSpaceTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/IgnoreWhiteSpaceTest.java index de5a6ba61b2b..c1e82c75368a 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/IgnoreWhiteSpaceTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/IgnoreWhiteSpaceTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl; import junit.framework.TestCase; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/LineFragmentsTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/LineFragmentsTest.java new file mode 100644 index 000000000000..321469832ef1 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/LineFragmentsTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2017 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.diff.impl; + +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.util.Assertion; + +import java.io.IOException; + +public class LineFragmentsTest extends BaseDiffTestCase { + private final Assertion CHECK = new Assertion(); + + public void testA() throws IOException { + setFile1("lines1.txt"); + setFile2("lines2.txt"); + DiffPanelImpl diffPanel = loadFiles(); + CHECK.compareAll(new int[]{1, 2, 3}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE1)); + CHECK.compareAll(new int[]{1, 2}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE2)); + } + + public void testC() throws IOException { + setFile1("linesC1.txt"); + setFile2("linesC2.txt"); + DiffPanelImpl diffPanel = loadFiles(); + checkTextEqual(content1(), getEditor1(diffPanel)); + checkTextEqual(content2(), getEditor2(diffPanel)); + } + + public void testD() throws IOException { + setFile1("linesD1.txt"); + setFile2("linesD2.txt"); + DiffPanelImpl diffPanel = loadFiles(); + CHECK.compareAll(new int[]{1, 2}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE1)); + CHECK.compareAll(new int[]{1, 2}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE2)); + } + + public void testEmptyLine() throws IOException { + setFile1("default/emptyLine.1"); + setFile2("default/emptyLine.2"); + DiffPanelImpl diffPanel = loadFiles(); + CHECK.compareAll(new int[]{1}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE1)); + CHECK.compareAll(new int[]{1}, diffPanel.getFragmentBeginnings(FragmentSide.SIDE2)); + } + + public void testRestyleNewLines() { + DiffPanelImpl diffPanel = createDiffPanel(null, myProject, false); + setContents(diffPanel, "f(a, b);\n", "f(a,\n b);\n"); + CHECK.singleElement(diffPanel.getFragmentBeginnings(FragmentSide.SIDE1), 0); + CHECK.singleElement(diffPanel.getFragmentBeginnings(FragmentSide.SIDE2), 0); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/MultiCheck.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/MultiCheck.java index 85d65b63a5c9..1d1e0a159e87 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/MultiCheck.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/MultiCheck.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl; import junit.framework.Assert; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/SingleTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/SingleTest.java new file mode 100644 index 000000000000..3ae351c6a750 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/SingleTest.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2017 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.diff.impl; + +import junit.framework.Test; + +public class SingleTest { + public static Test suite() { + return new DiffFilesTest.MyIdeaTestCase("wrappingBug", ComparisonPolicy.IGNORE_SPACE){}; + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/DocumentContentTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/DocumentContentTest.java new file mode 100644 index 000000000000..64c7d876aa63 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/DocumentContentTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2017 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.diff.impl.content; + +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.diff.DocumentContent; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.diff.impl.DiffPanelImpl; +import com.intellij.openapi.diff.impl.incrementalMerge.MergeTestUtils; +import com.intellij.openapi.editor.Document; + +public class DocumentContentTest extends BaseDiffTestCase { + public void testInitialSync() { + Document baseDocument = MergeTestUtils.createDocument("123"); + DocumentContent content = new DocumentContent(myProject, baseDocument); + content.addListener(SHOULD_NOT_INVALIDATE); + Document workingDocument = content.getDocument(); + replaceString(baseDocument, 0, 3, "xyz"); + //assertEquals("123", workingDocument.getText()); + DiffPanelImpl diffPanel = createDiffPanel(null, myProject, false); + diffPanel.setContents(content, new SimpleContent("1")); + assertEquals("xyz", workingDocument.getText()); + replaceString(baseDocument, 1, 2, "Y"); + assertEquals("xYz", workingDocument.getText()); + replaceString(baseDocument, 0, 3, ""); + assertEquals(0, workingDocument.getTextLength()); + replaceString(workingDocument, 0, 0, "123"); + assertEquals("123", baseDocument.getText()); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/FragmentContentTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/FragmentContentTest.java new file mode 100644 index 000000000000..db488fe66dcb --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/content/FragmentContentTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2000-2017 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.diff.impl.content; + +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.diff.DocumentContent; +import com.intellij.openapi.diff.FragmentContent; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.diff.impl.DiffPanelImpl; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.diff.impl.incrementalMerge.MergeFuncTest; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.containers.ContainerUtil; + +import javax.swing.*; + +public class FragmentContentTest extends BaseDiffTestCase { + private Document myDocument; + private FragmentContent myContent; + private DiffPanelImpl myDiffPanel; + private Document myFragment; + private DocumentContent myOriginalContent; + public static final Condition ACTION_HIGHLIGHTER = rangeHighlighter -> { + GutterIconRenderer iconRenderer = (GutterIconRenderer)rangeHighlighter.getGutterIconRenderer(); + if (iconRenderer == null) return false; + return iconRenderer.getClickAction() != null; + }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myDocument = createDocument("0123456789"); + myOriginalContent = new DocumentContent(myProject, myDocument); + myContent = new FragmentContent(myOriginalContent, new TextRange(3, 7), myProject, FileTypes.PLAIN_TEXT); + myDiffPanel = createDiffPanel(null, myProject, false); + myDiffPanel.setContents(myContent, new SimpleContent("1")); + myFragment = myContent.getDocument(); + myContent.addListener(SHOULD_NOT_INVALIDATE); + } + + @Override + protected void tearDown() throws Exception { + myDocument = null; + myContent = null; + myDiffPanel = null; + myFragment = null; + myOriginalContent = null; + super.tearDown(); + } + + public void testSynchonization() { + assertEquals("3456", myFragment.getText()); + replaceString(myFragment, 1, 3, "xy"); + assertEquals("0123xy6789", myDocument.getText()); + replaceString(myDocument, 4, 6, "45"); + assertEquals("0123456789", myOriginalContent.getDocument().getText()); + assertEquals("3456", myFragment.getText()); + replaceString(myDocument, 0, 1, "xyz"); + assertEquals("3456", myFragment.getText()); + replaceString(myFragment, 1, 3, "xy"); + assertEquals("xyz123xy6789", myDocument.getText()); + } + + public void testEditReadonlyDocument() { + SimpleContent content = new SimpleContent("abc"); + FragmentContent fragment = new FragmentContent(content, new TextRange(1, 2), myProject, FileTypes.PLAIN_TEXT); + fragment.onAssigned(true); + Document document = fragment.getDocument(); + assertEquals("b", document.getText()); + assertFalse(document.isWritable()); + assertFalse(content.getDocument().isWritable()); + fragment.onAssigned(false); + } + + public void testOriginalBecomesReadOnly() { + SimpleContent content = new SimpleContent("abc"); + content.setReadOnly(false); + FragmentContent fragment = new FragmentContent(content, new TextRange(1, 2), myProject, FileTypes.PLAIN_TEXT); + DiffPanelImpl diffPanel = createDiffPanel(null, myProject, false); + diffPanel.setContents(content, fragment); + JComponent component = diffPanel.getComponent(); + component.addNotify(); + assertNotNull(ContainerUtil.find(diffPanel.getEditor1().getMarkupModel().getAllHighlighters(), ACTION_HIGHLIGHTER)); + //fragment.onAssigned(true); + Document document = fragment.getDocument(); + content.getDocument().setReadOnly(true); + assertFalse(document.isWritable()); + component.removeNotify(); + } + + public void testRemoveOriginalFragment() { + myContent.removeListener(SHOULD_NOT_INVALIDATE); + Editor editor = myDiffPanel.getEditor(FragmentSide.SIDE2); + assertNotNull(MergeFuncTest.findAction(editor, 0, "")); + replaceString(myDocument, 2, 8, ""); + assertEquals("0189", myDocument.getText()); + assertNull(myDiffPanel.getEditor(FragmentSide.SIDE1)); + assertNull(MergeFuncTest.findAction(editor, 0, "")); + } + + private Document createDocument(String text) { + return EditorFactory.getInstance().createDocument(text); + } + + public void testRemoveListeners() { + replaceString(myFragment, 0, 1, "x"); + assertEquals("012x456789", myDocument.getText()); + myDiffPanel.setContents(new SimpleContent("1"), new SimpleContent("2")); + replaceString(myFragment, 0, 1, "3"); + assertEquals("012x456789", myDocument.getText()); + replaceString(myDocument, 3, 4, "y"); + assertEquals("012y456789", myDocument.getText()); + assertEquals("3456", myFragment.getText()); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/external/DiffManagerTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/external/DiffManagerTest.java index cfb07f5097c3..82f357902853 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/external/DiffManagerTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/external/DiffManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentEquality.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentEquality.java index a01a1cad021b..7ef409c63f86 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentEquality.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentEquality.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.highlighting; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentStringConvertion.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentStringConvertion.java index ef36ddb159e3..9a18b6bf5354 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentStringConvertion.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/FragmentStringConvertion.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.highlighting; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/LineBlockDividesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/LineBlockDividesTest.java index 1bf78de4261f..0896cbbb55d8 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/LineBlockDividesTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/LineBlockDividesTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.highlighting; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/MergeActionsTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/MergeActionsTest.java new file mode 100644 index 000000000000..a848a07bd711 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/MergeActionsTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2017 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.diff.impl.highlighting; + +import com.intellij.codeInsight.daemon.GutterMark; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.diff.impl.ComparisonPolicy; +import com.intellij.openapi.diff.impl.DiffFilesTest; +import com.intellij.openapi.diff.impl.DiffPanelImpl; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import junit.framework.Test; +import junit.framework.TestSuite; + +public class MergeActionsTest extends TestSuite { + public MergeActionsTest() { + addTestFile("_merge1"); + addTestFile("_merge2"); + } + + private void addTestFile(String fileName) { + addTest(new MyTestCase(fileName) {}); + } + + public static Test suite() { + return new MergeActionsTest(); + } + + public static abstract class MyTestCase extends DiffFilesTest.MyIdeaTestCase { + protected MyTestCase(String name) { + super(name, ComparisonPolicy.DEFAULT); + } + + @Override + protected void setContents(DiffPanelImpl diffPanel, String content1, String content2) { + SimpleContent diffContent1 = new SimpleContent(content1); + diffContent1.setReadOnly(false); + diffPanel.setContents(diffContent1, new SimpleContent(content2)); + } + + @Override + protected String process(Editor editor) { + StringBuffer buffer = new StringBuffer(); + RangeHighlighter[] allHighlighters = editor.getMarkupModel().getAllHighlighters(); + for (int i = 0; i < allHighlighters.length; i++) { + RangeHighlighter highlighter = allHighlighters[i]; + GutterMark iconRenderer = highlighter.getGutterIconRenderer(); + if (iconRenderer != null) { + buffer.append(iconRenderer.getTooltipText()); + buffer.append(' '); + buffer.append(highlighter.getStartOffset()); + buffer.append('-'); + buffer.append(highlighter.getEndOffset()); + buffer.append('\n'); + } + } + return buffer.toString(); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/UtilTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/UtilTest.java index 1d19fb92ee44..d3a7392e691d 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/UtilTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/highlighting/UtilTest.java @@ -1,8 +1,22 @@ +/* + * Copyright 2000-2017 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.diff.impl.highlighting; import com.intellij.openapi.diff.ex.DiffFragment; import com.intellij.openapi.diff.impl.MultiCheck; -import com.intellij.openapi.diff.impl.string.DiffString; import com.intellij.util.Assertion; import com.intellij.util.diff.Diff; import com.intellij.util.diff.FilesTooBigForDiffException; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/ChangeListTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/ChangeListTest.java new file mode 100644 index 000000000000..8a6d84430871 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/ChangeListTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2017 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.diff.impl.incrementalMerge; + +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.diff.impl.splitter.Interval; +import com.intellij.openapi.diff.impl.util.TextDiffType; +import com.intellij.openapi.diff.impl.util.TextDiffTypeEnum; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.Assertion; +import com.intellij.util.diff.FilesTooBigForDiffException; + +public class ChangeListTest extends PlatformTestCase { + private MergeTestUtils myUtils; + private final Assertion CHECK = new Assertion(); + + public void testInsMarkup() throws FilesTooBigForDiffException { + Document base = MergeTestUtils.createDocument("a\nb\nc\nd"); + Document version = MergeTestUtils.createRODocument("a\nIns\nb\nd"); + ChangeList changeList = buildChangeList(base, version); + Editor eBase = myUtils.createEditor(base); + Editor eVersion = myUtils.createEditor(version); + assertEquals(2, changeList.getCount()); + changeList.setMarkup(eBase, eVersion); + + MergeTestUtils.checkMarkup(eVersion, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 4), MergeTestUtils.del(8, 0)}); + MergeTestUtils.checkMarkup(eBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 0), MergeTestUtils.del(4, 2)}); + CHECK.compareAll(new TextDiffTypeEnum[]{TextDiffTypeEnum.INSERT, TextDiffTypeEnum.DELETED}, + convertTypesToEnums(changeList.getLineBlocks().getTypes())); + CHECK.compareAll(new Interval[]{new Interval(1, 0), new Interval(2, 1)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE1)); + CHECK.compareAll(new Interval[]{new Interval(1, 1), new Interval(3, 0)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE2)); + } + + private ChangeList buildChangeList(Document base, Document version) throws FilesTooBigForDiffException { + return ChangeList.build(base, version, getProject()); + } + + public void testInsAtEnd() throws FilesTooBigForDiffException { + Document base = MergeTestUtils.createDocument("a\n"); + Document version = MergeTestUtils.createRODocument("a\nIns"); + ChangeList changeList = buildChangeList(base, version); + Editor eBase = myUtils.createEditor(base); + Editor eVersion = myUtils.createEditor(version); + changeList.setMarkup(eBase, eVersion); + MergeTestUtils.checkMarkup(eVersion, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 3)}); + MergeTestUtils.checkMarkup(eBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 0)}); + CHECK.compareAll(new TextDiffTypeEnum[]{TextDiffTypeEnum.INSERT}, convertTypesToEnums(changeList.getLineBlocks().getTypes())); + CHECK.compareAll(new Interval[]{new Interval(1, 0)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE1)); + CHECK.compareAll(new Interval[]{new Interval(1, 1)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE2)); + + + version = MergeTestUtils.createRODocument("Ins\na"); + base = MergeTestUtils.createDocument("a"); + changeList = buildChangeList(base, version); + eBase = myUtils.createEditor(base); + eVersion = myUtils.createEditor(version); + changeList.setMarkup(eBase, eVersion); + MergeTestUtils.checkMarkup(eVersion, new MergeTestUtils.Range[]{MergeTestUtils.ins(0, 4)}); + MergeTestUtils.checkMarkup(eBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(0, 0)}); + CHECK.compareAll(new TextDiffTypeEnum[]{TextDiffTypeEnum.INSERT}, convertTypesToEnums(changeList.getLineBlocks().getTypes())); + CHECK.compareAll(new Interval[]{new Interval(0, 0)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE1)); + CHECK.compareAll(new Interval[]{new Interval(0, 1)}, + changeList.getLineBlocks().getIntervals(FragmentSide.SIDE2)); + } + + private static TextDiffTypeEnum[] convertTypesToEnums(TextDiffType[] types) { + TextDiffTypeEnum[] result = new TextDiffTypeEnum[types.length]; + for (int i = 0; i < types.length; i++) { + result[i] = types[i].getType(); + } + return result; + } + + public void testEditChange() throws FilesTooBigForDiffException { + Document base = MergeTestUtils.createDocument("a\nxx\nb\nyyy"); + Document version = MergeTestUtils.createRODocument("a\n1\n3\nb\nYYY"); + ChangeList changeList = buildChangeList(base, version); + Editor eBase = myUtils.createEditor(base); + Editor eVersion = myUtils.createEditor(version); + changeList.setMarkup(eBase, eVersion); + myUtils.insertString(base, 3, "\n"); + MergeTestUtils.checkMarkup(eBase, new MergeTestUtils.Range[]{MergeTestUtils.mod(2, 4), MergeTestUtils.mod(8, 3)}); + MergeTestUtils.checkMarkup(eVersion, new MergeTestUtils.Range[]{MergeTestUtils.mod(2, 4), MergeTestUtils.mod(8, 3)}); + Interval[] expected = {new Interval(1, 2), new Interval(4, 1)}; + CHECK.compareAll(new TextDiffTypeEnum[]{TextDiffTypeEnum.CHANGED, TextDiffTypeEnum.CHANGED}, + convertTypesToEnums(changeList.getLineBlocks().getTypes())); + CHECK.compareAll(expected, changeList.getLineBlocks().getIntervals(FragmentSide.SIDE1)); + CHECK.compareAll(expected, changeList.getLineBlocks().getIntervals(FragmentSide.SIDE2)); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myUtils = new MergeTestUtils(myProject); + } + + @Override + protected void tearDown() throws Exception { + try { + myUtils.tearDown(); + } + finally { + myUtils = null; + super.tearDown(); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeBuilderTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeBuilderTest.java index 61efdb64fd18..ab7097419411 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeBuilderTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeBuilderTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.incrementalMerge; import com.intellij.idea.IdeaLogger; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeFuncTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeFuncTest.java new file mode 100644 index 000000000000..9393daeef1cc --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeFuncTest.java @@ -0,0 +1,386 @@ +/* + * Copyright 2000-2017 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.diff.impl.incrementalMerge; + +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.impl.SimpleDataContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.diff.impl.incrementalMerge.ui.ApplyNonConflicts; +import com.intellij.openapi.diff.impl.splitter.Interval; +import com.intellij.openapi.diff.impl.splitter.LineBlocks; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.Assertion; +import com.intellij.util.diff.FilesTooBigForDiffException; +import org.jetbrains.annotations.NotNull; + +public class MergeFuncTest extends PlatformTestCase { + private MergeTestUtils myUtils; + private Document myLeft; + private Document myBase; + private Document myRight; + private Editor myELeft; + private Editor myEBase; + private Editor myERight; + private MergeList myMergeList; + private final Assertion CHECK = new Assertion(); + private ChangeCounter myCounters; + + public void testNoConflicts() throws FilesTooBigForDiffException { + useDocuments("a\nIns1\nb\nccc", + "a\nb\nccc", + "a\nb\ndddd"); + initMerge(); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 5)}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 0), MergeTestUtils.mod(4, 3)}); + MergeTestUtils.Range[] mRight = {MergeTestUtils.mod(4, 4)}; + MergeTestUtils.checkMarkup(myERight, mRight); + checkCounters(2, 0); + // Apply Ins1-> + pressApplyActionIcon(myELeft, 0); + MergeTestUtils.Range[] mLeft = new MergeTestUtils.Range[0]; + MergeTestUtils.checkMarkup(myELeft, mLeft); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.mod(9, 3)}); + MergeTestUtils.checkMarkup(myERight, mRight); + checkCounters(1, 0); + assertEquals("a\nIns1\nb\nccc", myBase.getText()); + checkApplyNoConflicts(true); + // Apply dddd->ccc + pressApplyActionIcon(myERight, 0); + MergeTestUtils.checkMarkup(myELeft, mLeft); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + checkCounters(0, 0); + assertEquals("a\nIns1\nb\ndddd", myBase.getText()); + checkApplyNoConflicts(false); + } + + private void checkCounters(int changes, int conflicts) { + ChangeCounter counters = ChangeCounter.getOrCreate(myMergeList); + if (myCounters == null) myCounters = counters; + else assertSame(myCounters, counters); + assertEquals(changes, myCounters.getChangeCounter()); + assertEquals(conflicts, myCounters.getConflictCounter()); + } + + public void testConflictingChange() throws FilesTooBigForDiffException { + useDocuments("1\n2\n3\nX\na\nb\nc\nY\nVer1\nVer12\nZ", + "X\na\nb\nc\nY\n" + "Ver12\nVer23\nZ", + "X\n" + "Y\n" + "Ver23\nVer3\nZ"); + initMerge(); + MergeTestUtils.Range leftConf = MergeTestUtils.conf(16, 11); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.ins(0, 6), leftConf}); + MergeTestUtils.Range rightConf = MergeTestUtils.conf(4, 11); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{MergeTestUtils.del(2, 0), rightConf}); + MergeTestUtils.Range baseConf = MergeTestUtils.conf(10, 12); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(0, 0), MergeTestUtils.del(2, 6), baseConf}); + checkCounters(2, 2); + + checkApplyNoConflicts(true); + runApplyNonConflicts(); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{leftConf}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{rightConf}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{baseConf}); + checkApplyNoConflicts(false); + checkCounters(0, 2); + } + + public void testApplyMergeThenUndo() throws FilesTooBigForDiffException { + String baseText = "X\n1\n2\n3\nY"; + useDocuments("X\nb\nY", baseText, "X\na\nY"); + initMerge(); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 2)}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 6)}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 2)}); + Interval[] baseLeftIntervals = getBaseIntervals(); + CHECK.compareAll(new Interval[]{new Interval(1, 3)}, baseLeftIntervals); + checkApplyNoConflicts(false); + + pressApplyActionIcon(myERight, 0); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[] { MergeTestUtils.conf(2, 2) }); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[] { MergeTestUtils.conf(4, 0)}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + + pressApplyActionIcon(myELeft, 0); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + assertEquals(0, getBaseIntervals().length); + checkApplyNoConflicts(false); + + undo(myEBase); + assertEquals(baseText, myBase.getText()); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + assertEquals(0, getBaseIntervals().length); + } + + public void testApplyModifiedDeletedConflict() throws FilesTooBigForDiffException { + String baseText = "X\n1\n2\n3\nY"; + useDocuments("X\nY", baseText, "X\na\nY"); + initMerge(); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 0)}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 6)}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 2)}); + Interval[] baseLeftIntervals = getBaseIntervals(); + CHECK.compareAll(new Interval[]{new Interval(1, 3)}, baseLeftIntervals); + checkApplyNoConflicts(false); + + pressApplyActionIcon(myERight, 0); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + + assertEquals(0, getBaseIntervals().length); + checkApplyNoConflicts(false); + + undo(myEBase); + assertEquals(baseText, myBase.getText()); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + assertEquals(0, getBaseIntervals().length); + } + + public void testInvalidatingChange() throws FilesTooBigForDiffException { + useDocuments("X\n1\n2\nY", "X\n1\nIns\n2\nY", "X\n1\n2\nY"); + initMerge(); + MergeTestUtils.Range[] sideConflict = {MergeTestUtils.del(4, 0)}; + MergeTestUtils.checkMarkup(myELeft, sideConflict); + MergeTestUtils.checkMarkup(myERight, sideConflict); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.del(4, 4)}); + CHECK.singleElement(getBaseIntervals(), new Interval(2, 1)); + + removeString(2, 10); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + assertEquals(0, getBaseIntervals().length); + } + + public void testApplySeveralActions() throws FilesTooBigForDiffException { + useDocuments("X\n1\nY\n2\nZ\n3\n4\nU\nW\n", + "X\na\nY\nb\nZ\nc\nU\nd\nW\n", + "X\na\nY\nB\nZ\nC\nU\nD\nW\n"); + initMerge(); + pressApplyActionIcon(myELeft, 0); + pressApplyActionIcon(myERight, 2); + assertEquals("X\n1\nY\nb\nZ\nc\nU\nD\nW\n", myBase.getText()); + pressApplyActionIcon(myERight, 0); + assertEquals("X\n1\nY\nB\nZ\nc\nU\nD\nW\n", myBase.getText()); + pressApplyActionIcon(myELeft, 0); + pressApplyActionIcon(myELeft, 0); + pressApplyActionIcon(myERight, 0); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + } + + public void testIgnoreChangeAction() throws FilesTooBigForDiffException { + useDocuments("X\n1\nY\n2\nZ", "X\na\nY\nb\nZ", "X\na\nY\nB\nZ"); + initMerge(); + pressIgnoreActionIcon(myELeft, 0); + assertEquals("X\na\nY\nb\nZ", myBase.getText()); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.conf(6, 2)}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.conf(6, 2)}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{MergeTestUtils.conf(6, 2)}); + pressIgnoreActionIcon(myERight, 0); + assertEquals("X\na\nY\nb\nZ", myBase.getText()); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + } + + public void testLongBase() throws FilesTooBigForDiffException { + useDocuments("X\n1\n2\n3\nZ", "X\n1\nb\n3\nd\ne\nf\nZ", "X\na\nb\nc\nZ"); + initMerge(); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 6)}); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 12)}); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[]{MergeTestUtils.conf(2, 6)}); + } + + public void testReplaceBaseWithBranch() throws FilesTooBigForDiffException { + String leftVersion = "a\nX\nb\nc"; + useDocuments(leftVersion, "A\nX\nB\nc", "1\nX\n1\nc"); + initMerge(); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.conf(0, 2), MergeTestUtils.conf(4, 2)}); + pressApplyActionIcon(myELeft, 0); + replaceString(0, myBase.getTextLength(), leftVersion); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + } + + public void testError1() throws FilesTooBigForDiffException { + useDocuments("start\n" + + "change\n" + + " a\n" + + " b", + "start\n" + + "CHANGE\n" + + " a\n" + + " b", + " }\n" + + " return new DiffFragment(notEmptyContent(buffer1), notEmptyContent(buffer2));\n" + + " }"); + initMerge(); + } + + public void testError2() throws FilesTooBigForDiffException { + useDocuments("C\nX", "C\n", "C\n"); + initMerge(); + MergeTestUtils.checkMarkup(myERight, new MergeTestUtils.Range[0]); + MergeTestUtils.checkMarkup(myEBase, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 0)}); + MergeTestUtils.checkMarkup(myELeft, new MergeTestUtils.Range[]{MergeTestUtils.ins(2, 1)}); + } + + public void testError3() throws FilesTooBigForDiffException { + useDocuments("original\nlocal\nlocal\nlocal\noriginal\n", + "original\noriginal\noriginal\noriginal\noriginal\n", + "original\nremote\nremote\nremote\noriginal\n"); + initMerge(); + } + + public void replaceString(final int start, final int end, final String text) { + ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(myProject, () -> { + myBase.deleteString(start, end); + myBase.insertString(start, text); + }, null, null)); + } + + private Interval[] getBaseIntervals() { + Interval[] left = getLineBlocks(FragmentSide.SIDE1, MergeList.BASE_SIDE); + Interval[] right = getLineBlocks(FragmentSide.SIDE2, MergeList.BASE_SIDE); + CHECK.compareAll(left, right); + return left; + } + + private void removeString(final int start, final int end) { + ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(myProject, () -> myBase.deleteString(start, end), null, null)); + } + + private Interval[] getLineBlocks(FragmentSide mergeSide, FragmentSide changeSide) { + return LineBlocks.fromChanges(myMergeList.getChanges(mergeSide).getChanges()).getIntervals(changeSide); + } + + private void undo(Editor editor) { + UndoManager undoManager = UndoManager.getInstance(myProject); + TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(editor); + assertTrue(undoManager.isUndoAvailable(textEditor)); + undoManager.undo(textEditor); + } + + private void useDocuments(String left, String base, String right) { + myLeft = MergeTestUtils.createRODocument(left); + myBase = MergeTestUtils.createDocument(base); + myRight = MergeTestUtils.createRODocument(right); + } + + private void initMerge() throws FilesTooBigForDiffException { + myMergeList = MergeList.create(myProject, myLeft, myBase, myRight); + Editor[] editors = myUtils.createEditors(new Document[]{myLeft, myBase, myRight}); + myELeft = editors[0]; + myEBase = editors[1]; + myERight = editors[2]; + myMergeList.setMarkups(myELeft, myEBase, myERight); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myUtils = new MergeTestUtils(myProject); + } + + @Override + protected void tearDown() throws Exception { + myUtils.tearDown(); + myUtils = null; + myMergeList = null; + myCounters = null; + myLeft = null; + myBase = null; + myRight = null; + myELeft = null; + myEBase = null; + myERight = null; + super.tearDown(); + } + + private static void pressApplyActionIcon(@NotNull Editor editor, int index) { + safeFindAction(editor, index, "ccept").actionPerformed(null); + } + + private static AnAction safeFindAction(@NotNull Editor editor, int index, String text) { + AnAction action = findAction(editor, index, text); + if (action == null) fail("Action not found: " + index); + return action; + } + + public static AnAction findAction(@NotNull Editor editor, int index, String text) { + RangeHighlighter[] highlighters = editor.getMarkupModel().getAllHighlighters(); + for (RangeHighlighter highlighter : highlighters) { + if (!highlighter.isValid()) continue; + GutterIconRenderer iconRenderer = highlighter.getGutterIconRenderer(); + if (iconRenderer == null) continue; + AnAction action = iconRenderer.getClickAction(); + if (action == null) continue; + if (!iconRenderer.getTooltipText().contains(text)) continue; + if (index == 0) { + return action; + } + else { + index--; + } + } + return null; + } + + private static void pressIgnoreActionIcon(Editor editor, int index) { + safeFindAction(editor, index, "gnore").actionPerformed(null); + } + + @Override + protected void invokeTestRunnable(@NotNull Runnable runnable) throws Exception { + runnable.run(); + } + + private void checkApplyNoConflicts(boolean isEnabled) { + AnActionEvent event = createApplyEvent(); + new ApplyNonConflicts(null).update(event); + Presentation presentation = event.getPresentation(); + assertEquals(isEnabled, presentation.isEnabled()); + } + + private AnActionEvent createApplyEvent() { + return new AnActionEvent(null, SimpleDataContext.getSimpleContext(MergeList.DATA_KEY.getName(), myMergeList), ActionPlaces.UNKNOWN, + new Presentation(), + ActionManager.getInstance(), + 0); + } + + public void runApplyNonConflicts() { + new ApplyNonConflicts(null).actionPerformed(createApplyEvent()); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeTestUtils.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeTestUtils.java new file mode 100644 index 000000000000..da3159be76e2 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/incrementalMerge/MergeTestUtils.java @@ -0,0 +1,263 @@ +/* + * Copyright 2000-2017 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.diff.impl.incrementalMerge; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.diff.DiffColors; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.editor.markup.GutterIconRenderer; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.editor.markup.SeparatorPlacement; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.Assertion; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Convertor; +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MergeTestUtils { + private final Project myProject; + private final ArrayList myEditorsToDispose = new ArrayList<>(); + private static final Assertion CHECK = new Assertion(); + + public static class Range { + private final String myId; + private final TextRange myRange; + + Range(String id, TextRange range) { + myId = id; + myRange = range; + } + + @Override + public String toString() { + return myId + " " + myRange; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Range range = (Range)o; + + if (!myId.equals(range.myId)) return false; + if (!myRange.equals(range.myRange)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myId.hashCode(); + result = 31 * result + myRange.hashCode(); + return result; + } + } + + public MergeTestUtils(Project project) { + myProject = project; + } + + public static void checkMarkup(Editor editor, Range[] expected) { + checkMarkup(editor, expected, null); + } + + public static void checkMarkup(Editor editor, Range[] expected, @Nullable Range[] expectedApplied) { + List allHighlighters = Arrays.asList(editor.getMarkupModel().getAllHighlighters()); + List changes = ContainerUtil.findAll(allHighlighters, CHANGE_HIGHLIGHTERS); + List appliedChanges = ContainerUtil.findAll(allHighlighters, APPLIED_CHANGE_HIGHLIGHTERS); + try { + checkMarkup(editor, changes, expected); + if (expectedApplied != null) { + checkMarkup(editor, appliedChanges, expectedApplied); + } + } catch(AssertionFailedError e) { + List ranges = ContainerUtil.map(allHighlighters, new HighlighterToRangeConvertor(editor)); + CHECK.enumerate(ranges); + throw e; + } + } + + private static void checkMarkup(Editor editor, List changes, Range[] expected) { + Function toRangeConvertor = new HighlighterToRangeConvertor(editor); + List actualRanges = ContainerUtil.map(changes, toRangeConvertor); + Assertion.compareUnordered(expected, actualRanges); + + for (RangeHighlighter highlighter : changes) { + if (highlighter.getStartOffset() == highlighter.getEndOffset()) continue; + Assert.assertEquals(Color.GRAY, highlighter.getLineSeparatorColor()); + Assert.assertEquals(SeparatorPlacement.TOP, highlighter.getLineSeparatorPlacement()); + List allHighlighters = Arrays.asList(editor.getMarkupModel().getAllHighlighters()); + RangeHighlighter bottomLine = findBottomHighlighter(highlighter, allHighlighters); + Assert.assertNotNull(String.format("The bottom line of %s is null!", toRangeConvertor.fun(highlighter)), bottomLine); + Assert.assertEquals(Color.GRAY, bottomLine.getLineSeparatorColor()); + } + } + + @Nullable + private static RangeHighlighter findBottomHighlighter(RangeHighlighter highlighter, List allHighlighters) { + int startOffset = highlighter.getStartOffset(); + int endOffset = highlighter.getEndOffset(); + if (highlighter.getDocument().getCharsSequence().charAt(endOffset - 1) == '\n') endOffset--; + for (RangeHighlighter rangeHighlighter : allHighlighters) { + if (rangeHighlighter.getStartOffset() != startOffset || rangeHighlighter.getEndOffset() != endOffset) continue; + if (!SeparatorPlacement.BOTTOM.equals(rangeHighlighter.getLineSeparatorPlacement())) continue; + return rangeHighlighter; + } + return null; + } + + public static Range ins(int start, int length) { + return new Range(DiffColors.DIFF_INSERTED.getExternalName(), createRange(start, length)); + } + + protected void tearDown() throws Exception { + EditorFactory editorFactory = EditorFactory.getInstance(); + for (Editor editor : myEditorsToDispose) { + editorFactory.releaseEditor(editor); + } + } + + public Editor[] createEditors(Document[] documents) { + Editor[] editors = new Editor[documents.length]; + for (int i = 0; i < documents.length; i++) { + Document document = documents[i]; + editors[i] = createEditor(document); + } + return editors; + } + + public Editor createEditor(Document document) { + Editor editor = EditorFactory.getInstance().createEditor(document); + myEditorsToDispose.add(editor); + return editor; + } + + private static final String[] POSSIBLE_ATTRIBUTES = + {DiffColors.DIFF_INSERTED.getExternalName(), + DiffColors.DIFF_DELETED.getExternalName(), + DiffColors.DIFF_MODIFIED.getExternalName(), + DiffColors.DIFF_CONFLICT.getExternalName()}; + + private static final Condition COMMON_CHANGE_HIGHLIGHTERS = + highlighter -> { + if (!highlighter.isValid()) return false; + if (highlighter.getLineSeparatorPlacement() == SeparatorPlacement.BOTTOM) return false; + GutterIconRenderer iconRenderer = (GutterIconRenderer)highlighter.getGutterIconRenderer(); + if (highlighter.getTextAttributes() == null && highlighter.getLineSeparatorColor() == null && + iconRenderer != null && iconRenderer.getClickAction() != null) return false; + return true; + }; + + private static final Condition CHANGE_HIGHLIGHTERS = + highlighter -> COMMON_CHANGE_HIGHLIGHTERS.value(highlighter) && !isAppliedChange(highlighter); + + private static final Condition APPLIED_CHANGE_HIGHLIGHTERS = + highlighter -> COMMON_CHANGE_HIGHLIGHTERS.value(highlighter) && isAppliedChange(highlighter); + + private static boolean isAppliedChange(RangeHighlighter highlighter) { + Color stripeMarkColor = highlighter.getErrorStripeMarkColor(); + return stripeMarkColor != null && stripeMarkColor.getAlpha() == ChangeHighlighterHolder.APPLIED_CHANGE_TRANSPARENCY; + } + + public static Document createRODocument(String text) { + Document document = createDocument(text); + document.setReadOnly(true); + return document; + } + + public static Document createDocument(String text) { + return EditorFactory.getInstance().createDocument(text); + } + + public static Range del(int start, int length) { + return new Range(DiffColors.DIFF_DELETED.getExternalName(), createRange(start, length)); + } + + private static TextRange createRange(int start, int length) { + return new TextRange(start, start + length); + } + + public static Range mod(int start, int length) { + return new Range(DiffColors.DIFF_MODIFIED.getExternalName(), createRange(start, length)); + } + + public static Range conf(int start, int length) { + return new Range(DiffColors.DIFF_CONFLICT.getExternalName(), createRange(start, length)); + } + + public void insertString(final Document document, final int offset, final String text) { + ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(myProject, () -> document.insertString(offset, text), null, null)); + } + + private static class ColorToIdConvertor implements Convertor { + private final Editor myEditor; + + public ColorToIdConvertor(Editor editor) { + myEditor = editor; + } + + @Override + public String convert(Color backgroundColor) { + for (String key : POSSIBLE_ATTRIBUTES) { + if (!backgroundColor.equals(getAttributes(key).getBackgroundColor())) continue; + return key; + } + return null; + } + + private TextAttributes getAttributes(String key) { + return myEditor.getColorsScheme().getAttributes(TextAttributesKey.find(key)); + } + } + + private static class HighlighterToRangeConvertor implements Function { + private final Convertor myColorToId; + + public HighlighterToRangeConvertor(Editor editor) { + myColorToId = new ColorToIdConvertor(editor); + } + + @Override + public Range fun(RangeHighlighter highlighter) { + TextAttributes textAttributes = highlighter.getTextAttributes(); + Color color; + if (textAttributes != null) color = textAttributes.getBackgroundColor(); + else color = highlighter.getLineSeparatorColor(); + String id; + if (Color.GRAY.equals(color)) id = "lineSeparator"; + else id = color != null ? myColorToId.convert(color) : highlighter.getGutterIconRenderer().getTooltipText(); + TextRange range = TextRange.create(highlighter); + return new Range(id, range); + } + } + +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/mergeTool/MergeDataTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/mergeTool/MergeDataTest.java new file mode 100644 index 000000000000..41389e0660c7 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/mergeTool/MergeDataTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2017 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.diff.impl.mergeTool; + +import com.intellij.openapi.diff.ActionButtonPresentation; +import com.intellij.openapi.diff.BaseDiffTestCase; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.TempFiles; + +import java.io.IOException; + +public class MergeDataTest extends BaseDiffTestCase { + private TempFiles myTempFiles; + + public void testWorkingDocument() throws IOException { + VirtualFile file = myTempFiles.createVFile("merge", ".txt"); + assertNotNull(file); + assertEquals("txt", file.getExtension()); + Document document = FileDocumentManager.getInstance().getDocument(file); + replaceString(document, 0, document.getTextLength(), "current"); + assertEquals("current", document.getText()); + MergeVersion.MergeDocumentVersion base = new MergeVersion.MergeDocumentVersion(document, "original"); + MergeRequestImpl mergeData = + new MergeRequestImpl("left", base, "right", myProject, ActionButtonPresentation.APPLY, ActionButtonPresentation.CANCEL_WITH_PROMPT); + assertEquals("left", mergeData.getContents()[0].getDocument().getText()); + Document workingDocument = mergeData.getContents()[1].getDocument(); + assertEquals("original", workingDocument.getText()); + assertEquals("right", mergeData.getContents()[2].getDocument().getText()); + replaceString(workingDocument, 0, workingDocument.getTextLength(), "corrected"); + mergeData.setResult(DialogWrapper.OK_EXIT_CODE); + assertEquals("corrected", document.getText()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myTempFiles = new TempFiles(myFilesToDelete); + } + + @Override + protected void tearDown() throws Exception { + myTempFiles.deleteAll(); + myTempFiles = null; + super.tearDown(); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchStatusTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchStatusTest.kt new file mode 100644 index 000000000000..7f9324f602a7 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchStatusTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 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.diff.impl.patch + +import com.intellij.openapi.diff.impl.patch.ApplyPatchStatus.PARTIAL +import org.junit.Test +import java.util.* +import kotlin.test.assertEquals + +class ApplyPatchStatusTest { + + @Test fun checkStatusAnd() { + ApplyPatchStatus.ORDERED_TYPES.forEach { typeA -> + ApplyPatchStatus.ORDERED_TYPES.forEach { typeB -> checkAndFor(typeA, typeB, getResult(typeA, typeB)); } + checkAndFor(typeA, null, typeA); + checkAndFor(null, typeA, typeA); + } + } + + private fun getResult(lhs: ApplyPatchStatus?, rhs: ApplyPatchStatus?): ApplyPatchStatus? { + if (lhs == null) return rhs; + if (rhs == null) return lhs; + if (lhs == rhs) return lhs; + if (ApplyPatchStatus.PARTIAL_ADDITIONAL_SET.containsAll(Arrays.asList(lhs, rhs))) return PARTIAL; + var index = Math.max(ApplyPatchStatus.ORDERED_TYPES.indexOf(lhs), ApplyPatchStatus.ORDERED_TYPES.indexOf(rhs)); + return ApplyPatchStatus.ORDERED_TYPES[index]; + } + + private fun checkAndFor(statusA: ApplyPatchStatus?, statusB: ApplyPatchStatus?, expectedResult: ApplyPatchStatus?) { + assertEquals(expectedResult, ApplyPatchStatus.and(statusA, statusB), "Bad result :$statusA $statusB"); + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchTest.java new file mode 100644 index 000000000000..4fe5e728fecf --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/ApplyPatchTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2000-2017 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.diff.impl.patch; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diff.impl.patch.formove.PatchApplier; +import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.vcs.changes.patch.ApplyPatchAction; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileFilter; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.PsiTestUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class ApplyPatchTest extends PlatformTestCase { + public void testAddLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testAddLastLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testOverlappingContext() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testAddFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testRemoveFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testMatchByContext() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testMultiFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testContextDiff() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testContextDiffAddLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testContextDiffRemoveLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testContextDiffMultiFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testEmptyLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testReversedNames() throws Exception { + doTest(0, ApplyPatchStatus.SUCCESS, null); + } + + public void testAlreadyApplied() throws Exception { + doTest(1, ApplyPatchStatus.ALREADY_APPLIED, null); + } + + public void testPartialApply() throws Exception { + doTest(1, ApplyPatchStatus.PARTIAL, null); + } + + public void testContextDiffSingleSpace() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testNoNewlineAtEof() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testContextNoNewlineAtEof() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testRenameFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testRenameDir() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, new VirtualFileFilter() { + @Override + public boolean accept(final VirtualFile file) { + return !"empty".equals(file.getNameWithoutExtension()); + } + }); + } + + public void testDeleteLastLineWithLineBreak() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testDeleteLineContentWithoutLineBreak() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testDeleteLastLineWithoutLineBreak() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyFileNoHunkAtEOF() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyFileRemoveLastEmptyLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyFileAddLastEmptyLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyFileLastLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyFileKeepLastEmptyLine() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testMoveFile() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testMoveFileWithoutRename() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testMoveAndRenameWithNameConflicts() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testIncorrectAlreadyAppliedDetection() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testOmittedChunkSize() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testWrongFileStartUnified() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testAddFileWithGitVersion() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testModifyLineWithGitVersion() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + public void testAddFileWithoutNewlineAtEOF() throws Exception { + doTest(1, ApplyPatchStatus.SUCCESS, null); + } + + private void doTest(final int skipTopDirs, final ApplyPatchStatus expectedStatus, final VirtualFileFilter fileFilter) throws Exception { + ApplicationManager.getApplication() + .runWriteAction(() -> FileTypeManager.getInstance().associate(FileTypes.PLAIN_TEXT, new ExtensionFileNameMatcher("old"))); + + String testDataRoot = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/"; + String testDataPath = testDataRoot + "/diff/applyPatch/" + getTestName(true); + String beforePath = testDataPath + "/before"; + String afterPath = testDataPath + "/after"; + VirtualFile afterDir = LocalFileSystem.getInstance().refreshAndFindFileByPath(afterPath.replace(File.separatorChar, '/')); + + VirtualFile patchedDir = PsiTestUtil.createTestProjectStructure(myProject, myModule, beforePath, myFilesToDelete); + + String patchPath = testDataPath + "/apply.patch"; + VirtualFile patchFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(patchPath.replace(File.separatorChar, '/')); + + PatchReader reader = PatchVirtualFileReader.create(patchFile); + List patches = new ArrayList<>(reader.readTextPatches()); + + ApplyPatchAction.applySkipDirs(patches, skipTopDirs); + final PatchApplier.ApplyPatchTask applyPart = + new PatchApplier(myProject, patchedDir, patches, null, null, null).createApplyPart(false, false); + applyPart.run(); + + assertEquals(expectedStatus, applyPart.getStatus()); + + PlatformTestUtil.assertDirectoriesEqual(patchedDir, afterDir, fileFilter); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/BinaryPatchTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/BinaryPatchTest.kt new file mode 100644 index 000000000000..6a93bdf23099 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/BinaryPatchTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2017 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.diff.impl.patch + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vcs.changes.patch.BinaryPatchWriter.writeBinaries +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.testFramework.PlatformTestCase +import com.intellij.testFramework.PlatformTestUtil +import java.io.File +import java.io.StringWriter +import java.util.* + +class BinaryPatchTest : PlatformTestCase() { + + var dataFileName = "data.bin" + var filePatchName = "file.patch" + + fun testAddedPng() { + doTest() + } + + fun testAddedEmptyPng() { + doTest() + } + + fun testAddedGif() { + doTest() + } + + fun testLen1() { + doTest() + + } + + fun testLen2() { + doTest() + } + + fun testLen3() { + doTest() + } + + fun testLetterXasLen() { + doTest() + } + + fun testReversePatchCreation() { + doTest(true) + } + + private fun doTest(reverse: Boolean = false) { + val testDataRoot = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/" + val testDataPath = "${testDataRoot}/diff/binaryPatch/${getTestName(true)}" + val dataFile = File(testDataPath, dataFileName) + dataFile.setExecutable(false) + val decodedContentBytes = FileUtil.loadFileBytes(dataFile) + val encodedFile = File(testDataPath, filePatchName) + val stringWriter = StringWriter() + val binaryPatch = if (reverse) BinaryFilePatch(decodedContentBytes, null) else BinaryFilePatch(null, decodedContentBytes) + binaryPatch.beforeName = dataFileName + binaryPatch.afterName = dataFileName + writeBinaries(testDataPath, listOf(binaryPatch), stringWriter) + assertEquals(FileUtil.loadFile(encodedFile, true), stringWriter.toString()) + val reader = PatchVirtualFileReader.create(VfsUtil.findFileByIoFile(encodedFile, true)) + reader.parseAllPatches() + val patches = reader.allPatches + assertTrue(patches.size == 1) + assertTrue(Arrays.equals(binaryPatch.afterContent, (patches.first() as BinaryFilePatch).afterContent)) + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/GenericApplyPatchTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/GenericApplyPatchTest.java new file mode 100644 index 000000000000..cf5fba821d90 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/GenericApplyPatchTest.java @@ -0,0 +1,841 @@ +/* + * Copyright 2000-2017 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.diff.impl.patch; + +import com.intellij.openapi.diff.impl.patch.apply.GenericPatchApplier; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vcs.changes.patch.AppliedTextPatch; +import com.intellij.openapi.vcs.changes.patch.AppliedTextPatch.HunkStatus; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.containers.ContainerUtil; +import org.junit.Assert; + +import java.util.*; + +public class GenericApplyPatchTest extends PlatformTestCase { + public void testSeveralSteps() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "7")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "10")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\n2\n3\n4\n7\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\n2\n5\n6\n9\n10\n11\naaa", after); + } + + public void testExchangedParts() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "2a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "7")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "7a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "10")); + + final GenericPatchApplier gap = new GenericPatchApplier("5\n6\n7\n8\n1\n2\n3\n4\n9\nextra line", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("5\n6a\n7a\n8\n1\n2a\n3a\n4\n9\nextra line", after); + } + + public void testDeleteAlmostOk() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n3\n4\n9\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n9\n8\n11\naaa", after); + } + + public void testInsertAlmostOk() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n9\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n3\n4\n5\n9\n8\n11\naaa", after); + } + + public void testInsertAlmostOkAlreadyApplied() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n3\n4\n9\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.ALREADY_APPLIED, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n3\n4\n5\n9\n8\n11\naaa", after); + } + + public void testChangeAlmostOk() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "b")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n3\n4\n9\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\na\nb\n9\n8\n11\naaa", after); + } + + public void testChangeAlmostOkAlreadyApplied() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "b")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\na\n9\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.ALREADY_APPLIED, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\na\nb\nc\n9\n8\n11\naaa", after); + } + + public void testChangeAlmostOkManySteps() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-1a")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-2a")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n4\n9\n8\n11" + + "\naaa\n2\n-1\n-2\n-3", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n4\n9\n8\n11\naaa\n2\n-1a\n-2a\n-3a\n-3b\n-4a\n-4b\n", after); + } + + public void testFirstNewLine() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 2, 1, 3); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + final GenericPatchApplier gap = new GenericPatchApplier("1\n2\n", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("\n1\n2\n", after); + } + + public void testNewEmptyLine() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 3, 1, 4); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "0")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\n1\n2\n", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\n\n1\n2\n", after); + } + + public void testInsertionsIntoTransformationsCoinsidence() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("5", "6", "7"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertTrue(transformations.isEmpty()); + Assert.assertEquals(ApplyPatchStatus.ALREADY_APPLIED, gap.getStatus()); + } + + public void testInsertionsIntoTransformationsInsert() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("5", "6", "ins", "7"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 5 && key.getEndOffset() == 5); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 2); + Assert.assertTrue("6".equals(list.get(0))); + Assert.assertTrue("ins".equals(list.get(1))); + } + + public void testInsertionsIntoTransformationsInsert0() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("ins", "5", "6", "7"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 4 && key.getEndOffset() == 4); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 2); + Assert.assertTrue("ins".equals(list.get(0))); + Assert.assertTrue("5".equals(list.get(1))); + } + + public void testInsertionsIntoTransformationsInsert1() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("5", "6", "7", "ins"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 6 && key.getEndOffset() == 6); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 2); + Assert.assertTrue("7".equals(list.get(0))); + Assert.assertTrue("ins".equals(list.get(1))); + } + + public void testInsertionsIntoTransformationsDeletion() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("5", "7"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 5 && key.getEndOffset() == 5); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 0); + } + + public void testInsertionsIntoTransformationsDeletion1() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("5", "6"); + // coincidence + gap.putCutIntoTransformations(new TextRange(4, 7), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 6 && key.getEndOffset() == 7); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 0); + } + + public void testInsertionsIntoTransformationsDeletion0() throws Exception { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + sb.append(String.valueOf(i + 1)).append('\n'); + } + final GenericPatchApplier gap = new GenericPatchApplier(sb.toString(), Collections.emptyList()); + final List strings = Arrays.asList("6", "7"); + // coincidence + gap.putCutIntoTransformations(new TextRange(3, 6), new GenericPatchApplier.MyAppliedData(strings, true, true, true, GenericPatchApplier.ChangeType.REPLACE)); + final TreeMap transformations = gap.getTransformations(); + Assert.assertFalse(transformations.isEmpty()); + Assert.assertEquals(1, transformations.size()); + + final Map.Entry entry = transformations.entrySet().iterator().next(); + final TextRange key = entry.getKey(); + Assert.assertTrue(key.getStartOffset() == 3 && key.getEndOffset() == 4); + final List list = entry.getValue().getList(); + Assert.assertTrue(list.size() == 0); + } + + public void testBetterContextMatch() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\n0\n0\n3\n4\n5\n\n\n5454\n5345\n2\n3\n4\n5\n543543", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + + final String after = gap.getAfter(); + Assert.assertEquals("0\n0\n0\n3\n4\n5\n\n\n5454\n5345\n2\na\nb\n543543", after); + } + + public void testBetterContextMatch1() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "543543")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\n0\n0\n3\n4\n5\n\n\n5454\n5345\n3\n4\n5\n11\n543543", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + + final String after = gap.getAfter(); + Assert.assertEquals("0\n0\n0\n3\n4\n5\n\n\n5454\n5345\na\nb\n11\n543543", after); + } + + public void testBetterContextMatch2() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\n0\n0\n2\n3\n4\n5\n\n\n5454\n5345\n2\n3\n4\n5\n11\n543543", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + + final String after = gap.getAfter(); + Assert.assertEquals("0\n0\n0\n2\n3\n4\n5\n\n\n5454\n5345\n2\na\nb\n11\n543543", after); + } + + public void testFromSecondLineManySteps() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-1a")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-2a")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n4\n9\n8\n11" + + "\naaa\n2\n-2\n-3", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n4\n9\n8\n11\naaa\n2\n-1a\n-2a\n-3a\n-3b\n-4a\n-4b\n", after); + } + + public void testFromSecondLineManySteps0() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-1*")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-1a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-1a*")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-2a")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-3b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "-4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "-4b")); + + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n-1*\n4\n9\n8\n11" + + "\naaa\n2\n-2\n-3\n-4", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\nfjsfsd\nqwhduhqwude\n\n2\n-1\n-1*\n4\n9\n8\n11\naaa\n2\n-1a\n-1a*\n-2a\n-3a\n-3b\n-4a\n-4b\n", after); + } + + public void testMoreInsertParts() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "3c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "3c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "10")); + + final GenericPatchApplier gap = new GenericPatchApplier("sdsad\nsdsad\n1c\n2c\n3c\n2r\n8\n9\n10\n", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("sdsad\nsdsad\n1c\n2c\n3c\n2r\n3a\n3c\n2r\n8\n9\n10\n", after); + } + + public void testMoreDeletionParts() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "3c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3a")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3c")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "2r")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "10")); + + final GenericPatchApplier gap = new GenericPatchApplier("sdsad\nsdsad\n1c\n2c\n3c\n2r\n3a\n3c\n2r\n8\n9\n10\n", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("sdsad\nsdsad\n1c\n2c\n3c\n2r\n8\n9\n10\n", after); + } + + public void testDoesNotMatchInTheEnd() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("0\nmmm\n2\n4\n7\n8\n11\naaa\n3\n3", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("0\nmmm\n2\n4\n7\n8\n11\naaa\n5\n6\n", after); + } + + // actually didn't catch the previous version + // nevertheless, should also pass + public void testDoesNotMatchAtStart() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3=")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3-")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("3-\n0\nmmm\n2\n4\n7\n8\n11\naaa", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("5\n6\n0\nmmm\n2\n4\n7\n8\n11\naaa", after); + } + + public void testOneLineInsertion() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + final PatchLine line = new PatchLine(PatchLine.Type.ADD, "5"); + line.setSuppressNewLine(true); + patchHunk.addLine(line); + + final GenericPatchApplier gap = new GenericPatchApplier("", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertTrue(result); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + + final String after = gap.getAfter(); + //this hunk would be applied as not bounded, it would be written at first, thus we will ignore no new line + Assert.assertEquals("5\n", after); + } + + public void testOneLineBadInsertion() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + final PatchLine line = new PatchLine(PatchLine.Type.ADD, "5"); + line.setSuppressNewLine(true); + patchHunk.addLine(line); + + final GenericPatchApplier gap = new GenericPatchApplier("7\n8\n5\n", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + + final String after = gap.getAfter(); + Assert.assertEquals("5\n7\n8\n5\n", after); + } + + public void testConflict1() throws Exception { + final PatchHunk patchHunk = new PatchHunk(1, 8, 1, 8); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3=")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + final GenericPatchApplier gap = new GenericPatchApplier("1\n2\n3=*7\n11\n12", Collections.singletonList(patchHunk)); + final boolean result = gap.execute(); + Assert.assertFalse(result); + Assert.assertEquals(ApplyPatchStatus.FAILURE, gap.getStatus()); + + gap.trySolveSomehow(); + + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("1\n2\n5\n3=*7\n11\n12", after); + } + + public void testAddAsFirst() throws Exception { + int[] beforeOfsetExpected = {1, 1}; + int[] afterOfsetExpected = {1, 2}; + final PatchHunk patchHunk = new PatchHunk(1, 4, 1, 5); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "7")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + + List hunks = GenericPatchApplier.SplitHunk.read(patchHunk); + assertEquals(2, hunks.size()); + for (int i = 0; i < hunks.size(); i++) { + assertEquals(beforeOfsetExpected[i], hunks.get(i).getStartLineBefore()); + assertEquals(afterOfsetExpected[i], hunks.get(i).getStartLineAfter()); + } + } + + public void testOffsets() throws Exception { + final PatchHunk patchHunk1 = new PatchHunk(1, 2, 1, 1); + patchHunk1.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk1.addLine(new PatchLine(PatchLine.Type.REMOVE, "2")); + + int[] beforeOfsetExpected = {3, 7, 9, 11}; + int[] afterOfsetExpected = {2, 6, 7, 9}; + final PatchHunk patchHunk2 = new PatchHunk(3, 12, 2, 11); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "7")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.REMOVE, "8")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.REMOVE, "10")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.ADD, "11")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.ADD, "13")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "14")); + patchHunk2.addLine(new PatchLine(PatchLine.Type.CONTEXT, "15")); + + List hunks1 = GenericPatchApplier.SplitHunk.read(patchHunk1); + List hunks2 = GenericPatchApplier.SplitHunk.read(patchHunk2); + assertEquals(1, hunks1.size()); + assertEquals(4, hunks2.size()); + final GenericPatchApplier.SplitHunk splitHunk = hunks1.get(0); + assertEquals(1, splitHunk.getStartLineBefore()); + assertEquals(1, splitHunk.getStartLineAfter()); + + for (int i = 0; i < hunks2.size(); i++) { + assertEquals(beforeOfsetExpected[i], hunks2.get(i).getStartLineBefore()); + assertEquals(afterOfsetExpected[i], hunks2.get(i).getStartLineAfter()); + } + } + + + public void testAlreadyApplied() throws Exception { + int[] beforeAppliedExpected = {2, 5, 6, 8}; + int[] endAppliedExpected = {4, 5, 7, 10}; + + final PatchHunk patchHunk = new PatchHunk(2, 12, 2, 12); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "7")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "10")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "13")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "13")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "14")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "15")); + + GenericPatchApplier gap = new GenericPatchApplier("1\n2\n5\n6\n7\n9\n11\n12\n13\n13\n14\n", Collections.singletonList(patchHunk)); + assertTrue(gap.execute()); + List appliedInfo = gap.getAppliedInfo(); + ContainerUtil.sort(appliedInfo, (o1, o2) -> Integer.compare(o1.getAppliedTo().start, o2.getAppliedTo().start)); + checkAppliedPositions(beforeAppliedExpected, endAppliedExpected, HunkStatus.ALREADY_APPLIED, appliedInfo); + + gap = new GenericPatchApplier("f\nd\n1768678\n2\n5\n6\n7\n9\n11\n12\n13\n13\n14\n", Collections.singletonList(patchHunk)); + assertTrue(gap.execute()); + appliedInfo = gap.getAppliedInfo(); + ContainerUtil.sort(appliedInfo, (o1, o2) -> Integer.compare(o1.getAppliedTo().start, o2.getAppliedTo().start)); + int[] beforeAppliedExpected2 = {4, 7, 8, 10}; + int[] endAppliedExpected2 = {6, 7, 9, 12}; + checkAppliedPositions(beforeAppliedExpected2, endAppliedExpected2, HunkStatus.ALREADY_APPLIED, appliedInfo); + } + + public void testExactlyApplied() throws Exception { + int[] beforeAppliedExpected = {5, 8, 10, 12}; + int[] afterAppliedExpected = {7, 9, 11, 12}; + final PatchHunk patchHunk = new PatchHunk(2, 12, 2, 12); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "6")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "7")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "8")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "9")); + patchHunk.addLine(new PatchLine(PatchLine.Type.REMOVE, "10")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "11")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "12")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "13")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "14")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "15")); + + final GenericPatchApplier gap = + new GenericPatchApplier("0\n0werewrewr\n\n1\n2\n3\n4\n7\n8\n9\n10\n12\n14\n", Collections.singletonList(patchHunk)); + assertTrue(gap.execute()); + checkAppliedPositions(beforeAppliedExpected, afterAppliedExpected, HunkStatus.EXACTLY_APPLIED, gap.getAppliedInfo()); + } + + public void testMatchBeforeStartOffset() throws Exception { + final PatchHunk patchHunk = new PatchHunk(2, 9, 2, 12); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a1")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c4")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a2")); + patchHunk.addLine(new PatchLine(PatchLine.Type.ADD, "a3")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c5")); + patchHunk.addLine(new PatchLine(PatchLine.Type.CONTEXT, "c6")); + + final GenericPatchApplier gap = + new GenericPatchApplier("c2\nc2\nc3\nc4\n\nc5\nc6\nw\nw\n\nAn\nw\n\n\n", Collections.singletonList(patchHunk)); + gap.execute(); + Assert.assertEquals(ApplyPatchStatus.SUCCESS, gap.getStatus()); + final String after = gap.getAfter(); + Assert.assertEquals("c2\nc2\nc3\na1\nc4\na2\na3\n\nc5\nc6\nw\nw\n\nAn\nw\n\n\n", after); + } + + private static void checkAppliedPositions(int[] beforeAppliedExpected, + int[] endAppliedExpected, HunkStatus expectedStatus, + List appliedInfo) { + for (int i = 0; i < appliedInfo.size(); i++) { + final AppliedTextPatch.AppliedSplitPatchHunk hunk = appliedInfo.get(i); + assertEquals(beforeAppliedExpected[i], hunk.getAppliedTo().start); + assertEquals(endAppliedExpected[i], hunk.getAppliedTo().end); + assertEquals(expectedStatus, hunk.getStatus()); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchBuilderTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchBuilderTest.java new file mode 100644 index 000000000000..91025bb77cc0 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchBuilderTest.java @@ -0,0 +1,257 @@ +/* + * Copyright 2000-2017 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. + */ + +/* + * Created by IntelliJ IDEA. + * User: yole + * Date: 03.11.2006 + * Time: 14:53:10 + */ +package com.intellij.openapi.diff.impl.patch; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.util.LineSeparator; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.hash.HashMap; +import com.intellij.vcsUtil.VcsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class PatchBuilderTest extends PlatformTestCase { + public void testAddFile() throws Exception { + doTest(); + } + + public void testAddFileForNullProject() throws Exception { + doTest(null, false); + } + + public void testAddFileNoNewline() throws Exception { + doTest(); + } + + public void testAddLine() throws Exception { + doTest(); + } + + public void testAddLineToEmptyFile() throws Exception { + doTest(); + } + + public void testAddLineToEmptyFileNoNewline() throws Exception { + doTest(); + } + + public void testAddNewlineAtEOF() throws Exception { + doTest(); + } + + public void testDeleteWholeFile() throws Exception { + doTest(); + } + + public void testDeleteWholeFileNoNewline() throws Exception { + doTest(); + } + + public void testModifyWithCRLF() throws Exception { + doTest(myProject, false, LineSeparator.CRLF.getSeparatorString()); + } + + public void testModifyLine() throws Exception { + doTest(); + } + + public void testModifyLineNoNewline() throws Exception { + doTest(); + } + + public void testModifyLineNoNewlineContext() throws Exception { + doTest(); + } + + public void testModifyNewline1() throws Exception { + doTest(); + } + + public void testModifyNewline2() throws Exception { + doTest(); + } + + public void testModifyNewline3() throws Exception { + doTest(); + } + + public void testModifyNewline4() throws Exception { + doTest(); + } + + public void testMultipleFiles() throws Exception { + doTest(myProject, true); + } + + public void testOverlappingContext() throws Exception { + doTest(); + } + + public void testRemoveFile() throws Exception { + doTest(); + } + + public void testRemoveFileNoNewline() throws Exception { + doTest(); + } + + public void testRemoveNewlineAtEOF() throws Exception { + doTest(); + } + + public void testSingleLine() throws Exception { + doTest(); + } + + public void testUnchangedFile() throws Exception { + doTest(myProject, true); + } + + private void doTest() throws IOException, VcsException { + doTest(myProject, false); + } + + private void doTest(@Nullable Project project, boolean relativePaths) throws IOException, VcsException { + doTest(project, relativePaths, null); + } + + private void doTest(@Nullable Project project, boolean relativePaths, @Nullable String forceLSeparator) throws IOException, VcsException { + String testDataRoot = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/"; + String testDataPath = testDataRoot + "diff/patch/" + getTestName(true); + assertTrue(new File(testDataPath).isDirectory()); + String beforePath = testDataPath + "/before"; + String afterPath = testDataPath + "/after"; + + List changes = new ArrayList<>(); + + Map beforeFileMap = new HashMap<>(); + Map afterFileMap = new HashMap<>(); + + File[] beforeFiles = FileUtil.notNullize(new File(beforePath).listFiles()); + for (File file : beforeFiles) { + beforeFileMap.put(file.getName(), file); + } + File[] afterFiles = FileUtil.notNullize(new File(afterPath).listFiles()); + for (File file : afterFiles) { + afterFileMap.put(file.getName(), file); + } + + Set files = ContainerUtil.union(beforeFileMap.keySet(), afterFileMap.keySet()); + for (String file : files) { + File beforeFile = beforeFileMap.get(file); + File afterFile = afterFileMap.get(file); + assert beforeFile != null || afterFile != null; + + ContentRevision beforeRevision = createRevision(beforeFile, "before", relativePaths); + ContentRevision afterRevision = createRevision(afterFile, "after", relativePaths); + changes.add(new Change(beforeRevision, afterRevision)); + } + + String expected = FileUtil.loadFile(new File(testDataPath, "expected.patch")); + + StringWriter writer = new StringWriter(); + List patches = IdeaTextPatchBuilder.buildPatch(project, changes, testDataPath, false); + UnifiedDiffWriter.write(project, patches, writer, forceLSeparator != null ? forceLSeparator : "\n", null); + String result = writer.toString(); + if (forceLSeparator == null) { + expected = StringUtil.convertLineSeparators(expected); + result = StringUtil.convertLineSeparators(result); + } + assertEquals(expected, result); + } + + @Nullable + private static MockContentRevision createRevision(@Nullable File file, + @NotNull String revision, + boolean relativePaths) { + if (file == null) return null; + String path = file.getPath(); + if (relativePaths) { + path = FileUtil.toSystemIndependentName(path).replace("/before/", "/"); + path = FileUtil.toSystemIndependentName(path).replace("/after/", "/"); + } + return new MockContentRevision(file, VcsUtil.getFilePath(path, false), revision); + } + + private static class MockContentRevision implements ContentRevision, VcsRevisionNumber { + private final File myFile; + private final FilePath myFilePath; + private final String myRevisionName; + + public MockContentRevision(@NotNull File file, @NotNull FilePath path, @NotNull String revisionName) { + myFile = file; + myFilePath = path; + myRevisionName = revisionName; + } + + @Override + @Nullable + public String getContent() throws VcsException { + try { + return FileUtil.loadFile(myFile); + } + catch (IOException ex) { + throw new VcsException(ex); + } + } + + @Override + @NotNull + public FilePath getFile() { + return myFilePath; + } + + @Override + @NotNull + public VcsRevisionNumber getRevisionNumber() { + return this; + } + + @Override + public String asString() { + return myRevisionName; + } + + @Override + public int compareTo(final VcsRevisionNumber o) { + return 0; + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchTextDetectionTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchTextDetectionTest.kt new file mode 100644 index 000000000000..a7704617df3f --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/patch/PatchTextDetectionTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2017 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.diff.impl.patch + +import com.intellij.openapi.fileEditor.impl.LoadTextUtil +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.testFramework.PlatformTestCase +import com.intellij.testFramework.PlatformTestUtil +import com.intellij.testFramework.PsiTestUtil +import java.io.File + +class PatchTextDetectionTest : PlatformTestCase() { + + fun testClassicalContextDiff() { + doTest(true) + } + + fun testClassicalUnifiedDiff() { + doTest(true) + } + + fun testContextDiffWithExtraInfo() { + doTest(true) + } + + fun testIdeaPatch() { + doTest(true) + } + + fun testRandomText() { + doTest(false) + } + + fun testNormalDiff() { + doTest(false) + } + + + private fun doTest(expected: Boolean) { + val testDataRoot = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/" + val testDataPath = testDataRoot + "/diff/patchTextDetection/" + getTestName(true) + PsiTestUtil.createTestProjectStructure(myProject, myModule, testDataPath, PlatformTestCase.myFilesToDelete) + val patchPath = testDataPath + "/test.patch" + val patchFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(patchPath.replace(File.separatorChar, '/')) + + val patchContents = patchFile!!.contentsToByteArray() + val patchText = LoadTextUtil.getTextByBinaryPresentation(patchContents, patchFile); + assertEquals(expected, PatchReader.isPatchContent((patchText.toString()))); + } + +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/ByWordTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/ByWordTest.java index a5386bc0e591..fb2906bae6a2 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/ByWordTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/ByWordTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/CorrectionTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/CorrectionTest.java index bcc2c3ef47c7..3ebf7fd8cb44 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/CorrectionTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/CorrectionTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/LineBlocksDiffPolicyTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/LineBlocksDiffPolicyTest.java index 2baf119ba240..efd8d1cddfcd 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/LineBlocksDiffPolicyTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/LineBlocksDiffPolicyTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/NormalizationTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/NormalizationTest.java index 0e1f22e7e917..a4bfd4e44cf3 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/NormalizationTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/NormalizationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/PreferWholeLinesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/PreferWholeLinesTest.java index a798bd975037..9b9e7bc88583 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/PreferWholeLinesTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/PreferWholeLinesTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/TextCompareProcessorTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/TextCompareProcessorTest.java index 854a17d47ebd..834c89ee911d 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/TextCompareProcessorTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/TextCompareProcessorTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.impl.ComparisonPolicy; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/UniteSameTypeTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/UniteSameTypeTest.java index 019124eb9a62..7c9b3c7b9dc0 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/UniteSameTypeTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/processing/UniteSameTypeTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.processing; import com.intellij.openapi.diff.ex.DiffFragment; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldedChangesTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldedChangesTest.java new file mode 100644 index 000000000000..3f30e943f42e --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldedChangesTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2017 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.diff.impl.splitter; + +import com.intellij.openapi.diff.impl.EditingSides; +import com.intellij.openapi.diff.impl.fragments.LineBlock; +import com.intellij.openapi.diff.impl.highlighting.FragmentSide; +import com.intellij.openapi.diff.impl.util.TextDiffTypeEnum; +import com.intellij.openapi.editor.Editor; + +import java.util.ArrayList; + +public class FoldedChangesTest extends FoldingTestCase { + private Editor myEditor1; + private Editor myEditor2; + private LineBlocks DELETED; + private static final int DIVIDER_POLYGON_OFFSET = 3; + + @Override + protected void setUp() throws Exception { + super.setUp(); + DELETED = LineBlocks.createLineBlocks(new LineBlock[]{new LineBlock(2, 2, 2, 0, TextDiffTypeEnum.DELETED)}); + + myEditor1 = createEditor(); + myEditor2 = createEditor(); + myEditor1.getComponent().setSize(100, 500); + myEditor2.getComponent().setSize(100, 500); + } + + @Override + protected void tearDown() throws Exception { + myEditor1 = null; + myEditor2 = null; + DELETED = null; + super.tearDown(); + } + + public void testInsertionToStart() { + addFolding(myEditor2, 2, 5); + checkPoligon(2, 4, 2, 2); + } + + private void checkPoligon(int start1, double end1, int start2, double end2) { + ArrayList poligons = DividerPolygon.createVisiblePolygons(new MyEditingSides(FragmentSide.SIDE1), + FragmentSide.SIDE1, DIVIDER_POLYGON_OFFSET); + assertEquals(1, poligons.size()); + check(poligons.get(0), start1, end1, start2, end2); + poligons = DividerPolygon.createVisiblePolygons(new MyEditingSides(FragmentSide.SIDE2), FragmentSide.SIDE2, DIVIDER_POLYGON_OFFSET); + assertEquals(1, poligons.size()); + check(poligons.get(0), start2, end2, start1, end1); + } + + public void testWholeInsertionToStart() { + addFolding(myEditor2, 2, 4); + checkPoligon(2, 4, 2, 2); + } + + public void testInsertionFromStartToStart() { + addFolding(myEditor1, 2, 6); + addFolding(myEditor2, 2, 6); + checkPoligon(2, 2.5, 2, 2); + } + + private void check(DividerPolygon poligon, int start1, double end1, int start2, double end2) { + int lineHeight = myEditor1.getLineHeight(); + assertEquals(new DividerPolygon(start1 * lineHeight - DIVIDER_POLYGON_OFFSET, + start2 * lineHeight - DIVIDER_POLYGON_OFFSET, + (int)(end1 * myEditor1.getLineHeight()) - DIVIDER_POLYGON_OFFSET, + (int)(end2 * lineHeight) - DIVIDER_POLYGON_OFFSET, + poligon.getColor(), false), poligon); + } + + private class MyEditingSides implements EditingSides { + private final FragmentSide myLeft; + + public MyEditingSides(FragmentSide left) { + myLeft = left; + } + + @Override + public Editor getEditor(FragmentSide side) { + if (myLeft == side) return myEditor1; + if (myLeft.otherSide() == side) return myEditor2; + throw side.invalidException(); + } + + @Override + public LineBlocks getLineBlocks() { + return DELETED; + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTestCase.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTestCase.java new file mode 100644 index 000000000000..2e2f85a7e610 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTestCase.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2017 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.diff.impl.splitter; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.FoldRegion; +import com.intellij.openapi.editor.FoldingModel; +import com.intellij.testFramework.PlatformTestCase; + +import java.util.ArrayList; + +public abstract class FoldingTestCase extends PlatformTestCase { + private final ArrayList myEditorsToDispose = new ArrayList<>(); + + protected static void addFolding(Editor editor, final int startOffset, final int endOffset) { + final FoldingModel foldingModel = editor.getFoldingModel(); + foldingModel.runBatchFoldingOperation(() -> { + final FoldRegion foldRegion = foldingModel.addFoldRegion(startOffset, endOffset, ""); + if (foldRegion == null) return ; + foldRegion.setExpanded(false); + assertFalse(foldRegion.isExpanded()); + }); + } + + protected Editor createEditor() { + EditorFactory editorFactory = EditorFactory.getInstance(); + Editor editor = editorFactory.createEditor(editorFactory.createDocument("\n\n\n\n\n\n\n\n\n\n")); + editor.getComponent().setSize(100, 500); + myEditorsToDispose.add(editor); + return editor; + } + + @Override + protected void tearDown() throws Exception { + try { + for (Editor editor : myEditorsToDispose) { + EditorFactory.getInstance().releaseEditor(editor); + } + myEditorsToDispose.clear(); + } + finally { + super.tearDown(); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTransformationTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTransformationTest.java new file mode 100644 index 000000000000..106c0f481941 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/FoldingTransformationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2017 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.diff.impl.splitter; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; + + +public class FoldingTransformationTest extends FoldingTestCase { + private Editor myEditor; + private Document myDocument; + private Transformation myTransformation; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myEditor = createEditor(); + myDocument = myEditor.getDocument(); + } + + @Override + protected void tearDown() throws Exception { + myEditor = null; + myDocument = null; + myTransformation = null; + super.tearDown(); + } + + private void createTransformation() { + myTransformation = new FoldingTransformation(myEditor); + } + + public void testNoFolding() { + createTransformation(); + Interval interval = DividerPolygon.getVisibleInterval(myEditor); + for (int i = interval.getStart(); i < Math.min(myDocument.getLineCount(), interval.getEnd()); i++) { + assertEquals(i * getLineHeight(), myTransformation.transform(i)); + } + } + + private int getLineHeight() { + return myEditor.getLineHeight(); + } + + public void testFolderRegion() { + addFolding(myEditor, 3, 7); + createTransformation(); + int start = myTransformation.transform(3); + int lineHeight = getLineHeight(); + assertEquals(3 * lineHeight, start); + int end = myTransformation.transform(8); + assertEquals(4 * lineHeight, end); + int middle = myTransformation.transform(5); + assertEquals((double)(end + start) / 2, middle, 0.5); + int below = myTransformation.transform(4); + assertTrue(below < middle); + int above = myTransformation.transform(6); + assertTrue(above > middle); + assertTrue(above < end); + assertTrue(below > start); + assertEquals(start - lineHeight, myTransformation.transform(2)); + assertEquals(end + lineHeight, myTransformation.transform(9)); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/LineBlocksTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/LineBlocksTest.java index b96b2e3e6ee0..188ce92383dd 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/LineBlocksTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/LineBlocksTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.splitter; import com.intellij.openapi.diff.impl.fragments.LineBlock; diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/TransformationTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/TransformationTest.java index 1f90233dfd07..37eca777332c 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/TransformationTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/diff/impl/splitter/TransformationTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2017 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.diff.impl.splitter; import junit.framework.TestCase; diff --git a/platform/util/src/com/intellij/openapi/diff/impl/processing/DiffFragmentsProcessor.java b/platform/util/src/com/intellij/openapi/diff/impl/processing/DiffFragmentsProcessor.java deleted file mode 100644 index a4dad84c9cec..000000000000 --- a/platform/util/src/com/intellij/openapi/diff/impl/processing/DiffFragmentsProcessor.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2000-2009 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.diff.impl.processing; - -import com.intellij.openapi.diff.ex.DiffFragment; -import com.intellij.openapi.diff.impl.fragments.LineFragment; - -import java.util.ArrayList; - -public class DiffFragmentsProcessor { - public ArrayList process(DiffFragment[] fragments) { - LineFragmentsCollector collector = new LineFragmentsCollector(); - for (int i = 0; i < fragments.length; i++) { - DiffFragment fragment = fragments[i]; - collector.addDiffFragment(fragment); - } - return collector.getFragments(); - } -} diff --git a/platform/util/src/com/intellij/openapi/diff/impl/processing/DummyDiffFragmentsProcessor.java b/platform/util/src/com/intellij/openapi/diff/impl/processing/DummyDiffFragmentsProcessor.java index 8da7510145b8..5be87b5d2b9c 100644 --- a/platform/util/src/com/intellij/openapi/diff/impl/processing/DummyDiffFragmentsProcessor.java +++ b/platform/util/src/com/intellij/openapi/diff/impl/processing/DummyDiffFragmentsProcessor.java @@ -22,7 +22,7 @@ import com.intellij.openapi.util.text.StringUtil; import java.util.ArrayList; -public class DummyDiffFragmentsProcessor { +class DummyDiffFragmentsProcessor { public ArrayList process(String text1, String text2) { ArrayList lineFragments = new ArrayList(); diff --git a/platform/util/src/com/intellij/openapi/diff/impl/processing/TextCompareProcessor.java b/platform/util/src/com/intellij/openapi/diff/impl/processing/TextCompareProcessor.java index 164ee0d1ba45..ee54e7818e89 100644 --- a/platform/util/src/com/intellij/openapi/diff/impl/processing/TextCompareProcessor.java +++ b/platform/util/src/com/intellij/openapi/diff/impl/processing/TextCompareProcessor.java @@ -73,7 +73,7 @@ public class TextCompareProcessor { DiffFragment[] woFormattingBlocks = myDiffPolicy.buildFragments(diffText1, diffText2); DiffFragment[] step1lineFragments = new DiffCorrection.TrueLineBlocks(myComparisonPolicy).correctAndNormalize(woFormattingBlocks); - ArrayList lineBlocks = new DiffFragmentsProcessor().process(step1lineFragments); + ArrayList lineBlocks = processFragments(step1lineFragments); int badLinesCount = 0; if (myHighlightMode == HighlightMode.BY_WORD) { @@ -119,6 +119,14 @@ public class TextCompareProcessor { return collector.getFragments(); } + private static ArrayList processFragments(DiffFragment[] fragments) { + LineFragmentsCollector collector = new LineFragmentsCollector(); + for (DiffFragment fragment : fragments) { + collector.addDiffFragment(fragment); + } + return collector.getFragments(); + } + private static ArrayList processInlineFragments(DiffFragment[] subLineFragments) { LOG.assertTrue(subLineFragments.length > 0); FragmentsCollector result = new FragmentsCollector(); diff --git a/platform/util/util.iml b/platform/util/util.iml index ba5bce845778..b52f0341eb7c 100644 --- a/platform/util/util.iml +++ b/platform/util/util.iml @@ -23,8 +23,6 @@ - - diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcs.java b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcs.java index 8eddb671f818..05b0343c2423 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcs.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcs.java @@ -74,14 +74,6 @@ public abstract class AbstractVcs extends S myKey = new VcsKey(myName); } - // for tests only - protected AbstractVcs(@NotNull Project project, String name, VcsKey key) { - super(); - myProject = project; - myName = name; - myKey = key; - } - // acts as adapter @Override protected void start() throws VcsException { @@ -133,9 +125,6 @@ public abstract class AbstractVcs extends S return null; } - public void directoryMappingChanged() { - } - public boolean markExternalChangesAsUpToDate() { return false; } @@ -408,14 +397,6 @@ public abstract class AbstractVcs extends S return false; } - /** - * If VCS does not implement detection whether directory is versioned ({@link #isVersionedDirectory(VirtualFile)}), - * it should return {@code false}. Otherwise return {@code true} - */ - public boolean supportsVersionedStateDetection() { - return true; - } - /** * Returns the configurable to be shown in the VCS directory mapping dialog which should be displayed * for configuring VCS-specific settings for the specified root, or null if no such configuration is required. @@ -558,14 +539,6 @@ public abstract class AbstractVcs extends S setRollbackEnvironment(createRollbackEnvironment()); } - /** - * @Deprecated to delete in 2017.3 - */ - @Deprecated - public boolean reportsIgnoredDirectories() { - return true; - } - @Nullable public CommittedChangeList loadRevisions(final VirtualFile vf, final VcsRevisionNumber number) { final CommittedChangeList[] list = new CommittedChangeList[1]; @@ -603,13 +576,6 @@ public abstract class AbstractVcs extends S return true; } - /** - * compares different presentations of revision number (ex. in Perforce) - */ - public boolean revisionsSame(@NotNull final VcsRevisionNumber number1, @NotNull final VcsRevisionNumber number2) { - return number1.equals(number2); - } - public CheckoutProvider getCheckoutProvider() { return null; } diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java index b4d29cf25692..25073ef3e66a 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java @@ -23,7 +23,6 @@ import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.CommitResultHandler; import com.intellij.openapi.vcs.changes.LocalChangeList; -import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsHistoryProvider; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; @@ -36,7 +35,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; -import java.io.File; import java.util.*; import java.util.List; @@ -74,8 +72,6 @@ public abstract class AbstractVcsHelper { public abstract void showAnnotation(FileAnnotation annotation, VirtualFile file, AbstractVcs vcs, int line); - public abstract void showDifferences(final VcsFileRevision cvsVersionOn, final VcsFileRevision cvsVersionOn1, final File file); - public abstract void showChangesListBrowser(CommittedChangeList changelist, @Nls String title); public void showChangesListBrowser(CommittedChangeList changelist, @Nullable VirtualFile toSelect, @Nls String title) { @@ -93,10 +89,6 @@ public abstract class AbstractVcsHelper { public abstract void showWhatDiffersBrowser(@Nullable Component parent, Collection changes, @Nls String title); - @Nullable - public abstract T chooseCommittedChangeList(@NotNull CommittedChangesProvider provider, - RepositoryLocation location); - public abstract void openCommittedChangesTab(AbstractVcs vcs, VirtualFile root, ChangeBrowserSettings settings, diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/MultipleChangeListBrowser.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/MultipleChangeListBrowser.java index 879ce17795e4..1be8f72d9b2c 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/MultipleChangeListBrowser.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/MultipleChangeListBrowser.java @@ -85,7 +85,6 @@ public class MultipleChangeListBrowser extends ChangesBrowserBase { setInitialSelection(changeLists, changes, initialListSelection); myChangeListChooser = new ChangeListChooser(); - myChangeListChooser.updateLists(changeLists); myHeaderPanel.add(myChangeListChooser, BorderLayout.EAST); ChangeListManager.getInstance(myProject).addChangeListListener(myChangeListListener); diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/RearrangeBeforeCheckinHandler.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/RearrangeBeforeCheckinHandler.java index 659a26d372db..42244657a3f7 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/RearrangeBeforeCheckinHandler.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/RearrangeBeforeCheckinHandler.java @@ -86,9 +86,8 @@ public class RearrangeBeforeCheckinHandler extends CheckinHandler implements Che }; if (VcsConfiguration.getInstance(myProject).REARRANGE_BEFORE_PROJECT_COMMIT && !DumbService.isDumb(myProject)) { - new RearrangeCodeProcessor( - myProject, CheckinHandlerUtil.getPsiFiles(myProject, myPanel.getVirtualFiles()), COMMAND_NAME, performCheckoutAction - ).run(); + new RearrangeCodeProcessor(myProject, CheckinHandlerUtil.getPsiFiles(myProject, myPanel.getVirtualFiles()), COMMAND_NAME, + performCheckoutAction, true).run(); } else { performCheckoutAction.run(); diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/TodoCheckinHandlerWorker.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/TodoCheckinHandlerWorker.java index c32724da3006..f714fe8ebbe9 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/TodoCheckinHandlerWorker.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/checkin/TodoCheckinHandlerWorker.java @@ -15,23 +15,21 @@ */ package com.intellij.openapi.vcs.checkin; +import com.intellij.diff.comparison.ComparisonManager; +import com.intellij.diff.comparison.ComparisonPolicy; +import com.intellij.diff.comparison.DiffTooBigException; +import com.intellij.diff.fragments.LineFragment; +import com.intellij.diff.util.DiffUtil; +import com.intellij.diff.util.TextDiffType; import com.intellij.ide.todo.TodoFilter; import com.intellij.ide.todo.TodoIndexPatternProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.diff.ex.DiffFragment; -import com.intellij.openapi.diff.impl.ComparisonPolicy; -import com.intellij.openapi.diff.impl.fragments.LineFragment; -import com.intellij.openapi.diff.impl.highlighting.FragmentSide; -import com.intellij.openapi.diff.impl.processing.DiffCorrection; -import com.intellij.openapi.diff.impl.processing.DiffFragmentsProcessor; -import com.intellij.openapi.diff.impl.processing.DiffPolicy; -import com.intellij.openapi.diff.impl.string.DiffString; -import com.intellij.openapi.diff.impl.util.TextDiffTypeEnum; +import com.intellij.openapi.progress.DumbProgressIndicator; +import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.Getter; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; @@ -51,13 +49,15 @@ import com.intellij.psi.search.TodoItem; import com.intellij.psi.search.searches.IndexPatternSearch; import com.intellij.util.PairConsumer; import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Convertor; -import com.intellij.util.diff.FilesTooBigForDiffException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import static com.intellij.util.ObjectUtils.notNull; + /** * @author irengrig * Date: 2/18/11 @@ -210,42 +210,23 @@ public class TodoCheckinHandlerWorker { myAcceptor.skipped(Pair.create(myAfterFile, ourCannotLoadPreviousRevision)); return; } - ArrayList lineFragments = getLineFragments(myAfterFile.getPath(), myBeforeContent, myAfterContent); - for (Iterator iterator = lineFragments.iterator(); iterator.hasNext(); ) { + List lineFragments = getLineFragments(myAfterFile.getPath(), myBeforeContent, myAfterContent); + lineFragments = ContainerUtil.filter(lineFragments, it -> DiffUtil.getLineDiffType(it) != TextDiffType.DELETED); + final StepIntersection intersection = new StepIntersection<>( + TODO_ITEM_CONVERTOR, LINE_FRAGMENT_CONVERTOR, lineFragments); + + intersection.process(newTodoItems, (todoItem, lineFragment) -> { ProgressManager.checkCanceled(); - final LineFragment next = iterator.next(); - final TextDiffTypeEnum type = next.getType(); - assert ! TextDiffTypeEnum.CONFLICT.equals(type); - if (type == null || TextDiffTypeEnum.DELETED.equals(type) || TextDiffTypeEnum.NONE.equals(type)) { - iterator.remove(); + if (myCurrentLineFragment == null || myCurrentLineFragment != lineFragment) { + myCurrentLineFragment = lineFragment; + myOldTodoTexts = null; } - } - final StepIntersection intersection = - new StepIntersection<>(TodoItemConvertor.getInstance(), LineFragmentConvertor.getInstance(), lineFragments, - new Getter() { - @Override - public String get() { - return myAfterContent; - } - }); - - intersection.process(newTodoItems, new PairConsumer() { - - @Override - public void consume(TodoItem todoItem, LineFragment lineFragment) { - ProgressManager.checkCanceled(); - if (myCurrentLineFragment == null || ! myCurrentLineFragment.getRange(FragmentSide.SIDE2).equals( - lineFragment.getRange(FragmentSide.SIDE2))) { - myCurrentLineFragment = lineFragment; - myOldTodoTexts = null; - } - final TextDiffTypeEnum type = lineFragment.getType(); - if (TextDiffTypeEnum.INSERT.equals(type)) { - myAcceptor.addedOrEdited(todoItem); - } else { - // change - checkEditedFragment(todoItem); - } + if (DiffUtil.getLineDiffType(lineFragment) == TextDiffType.INSERTED) { + myAcceptor.addedOrEdited(todoItem); + } + else { + // change + checkEditedFragment(todoItem); } }); } catch (VcsException e) { @@ -281,12 +262,7 @@ public class TodoCheckinHandlerWorker { } if (myOldTodoTexts == null) { final StepIntersection intersection = new StepIntersection<>( - LineFragmentConvertor.getInstance(), TodoItemConvertor.getInstance(), myOldItems, new Getter() { - @Override - public String get() { - return myBeforeContent; - } - }); + LINE_FRAGMENT_CONVERTOR, TODO_ITEM_CONVERTOR, myOldItems); myOldTodoTexts = new HashSet<>(); intersection.process(Collections.singletonList(myCurrentLineFragment), new PairConsumer() { @Override @@ -327,14 +303,13 @@ public class TodoCheckinHandlerWorker { return StringUtil.join(fragment.split("\\s"), " "); } - private static ArrayList getLineFragments(final String fileName, String beforeContent, String afterContent) throws VcsException { + private static List getLineFragments(@NotNull String fileName, @NotNull String beforeContent, @NotNull String afterContent) + throws VcsException { try { - DiffFragment[] woFormattingBlocks = - DiffPolicy.LINES_WO_FORMATTING.buildFragments(DiffString.create(beforeContent), DiffString.create(afterContent)); - DiffFragment[] step1lineFragments = - new DiffCorrection.TrueLineBlocks(ComparisonPolicy.IGNORE_SPACE).correctAndNormalize(woFormattingBlocks); - return new DiffFragmentsProcessor().process(step1lineFragments); - } catch (FilesTooBigForDiffException e) { + ProgressIndicator indicator = notNull(ProgressManager.getInstance().getProgressIndicator(), DumbProgressIndicator.INSTANCE); + return ComparisonManager.getInstance().compareLines(beforeContent, afterContent, ComparisonPolicy.IGNORE_WHITESPACES, indicator); + } + catch (DiffTooBigException e) { throw new VcsException("File " + fileName + " is too big and there are too many changes to build a diff", e); } } @@ -343,33 +318,16 @@ public class TodoCheckinHandlerWorker { private final static String ourCannotLoadPreviousRevision = "Can not load previous revision"; private final static String ourCannotLoadCurrentRevision = "Can not load current revision"; - private static class TodoItemConvertor implements Convertor { - private static final TodoItemConvertor ourInstance = new TodoItemConvertor(); + private static final Convertor TODO_ITEM_CONVERTOR = o -> { + final TextRange textRange = o.getTextRange(); + return new TextRange(textRange.getStartOffset(), textRange.getEndOffset() - 1); + }; - public static TodoItemConvertor getInstance() { - return ourInstance; - } - - @Override - public TextRange convert(TodoItem o) { - final TextRange textRange = o.getTextRange(); - return new TextRange(textRange.getStartOffset(), textRange.getEndOffset() - 1); - } - } - - private static class LineFragmentConvertor implements Convertor { - private static final LineFragmentConvertor ourInstance = new LineFragmentConvertor(); - - public static LineFragmentConvertor getInstance() { - return ourInstance; - } - - @Override - public TextRange convert(LineFragment o) { - final TextRange textRange = o.getRange(FragmentSide.SIDE2); - return new TextRange(textRange.getStartOffset(), textRange.getEndOffset() - 1); - } - } + private static final Convertor LINE_FRAGMENT_CONVERTOR = o -> { + int start = o.getStartOffset2(); + int end = o.getEndOffset2(); + return new TextRange(start, Math.max(start, end - 1)); + }; public List inOneList() { final List list = new ArrayList<>(); diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java index bc3706c6bb18..1895706810bc 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java @@ -26,11 +26,11 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.diff.*; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.*; -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; @@ -39,7 +39,6 @@ import com.intellij.openapi.progress.impl.CoreProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Getter; import com.intellij.openapi.util.Pair; @@ -62,7 +61,6 @@ import com.intellij.openapi.vcs.versionBrowser.ChangesBrowserSettingsEditor; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.openapi.vcs.vfs.VcsFileSystem; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; @@ -88,8 +86,6 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.awt.*; -import java.io.File; -import java.io.IOException; import java.text.MessageFormat; import java.util.*; import java.util.List; @@ -426,47 +422,6 @@ public class AbstractVcsHelperImpl extends AbstractVcsHelper { AnnotateToggleAction.doAnnotate(editor, myProject, file, annotation, vcs); } - public void showDifferences(final VcsFileRevision version1, final VcsFileRevision version2, final File file) { - try { - final byte[] byteContent1 = VcsHistoryUtil.loadRevisionContent(version1); - final byte[] byteContent2 = VcsHistoryUtil.loadRevisionContent(version2); - - if (Comparing.equal(byteContent1, byteContent2)) { - Messages.showInfoMessage(VcsBundle.message("message.text.versions.are.identical"), VcsBundle.message("message.title.diff")); - } - - final SimpleDiffRequest request = new SimpleDiffRequest(myProject, file.getAbsolutePath()); - - final FileType fileType = FileTypeManager.getInstance().getFileTypeByFileName(file.getName()); - if (fileType.isBinary()) { - Messages.showInfoMessage(VcsBundle.message("message.text.binary.versions.differ"), VcsBundle.message("message.title.diff")); - - return; - } - - final DiffContent content1 = getContentForVersion(version1, file); - final DiffContent content2 = getContentForVersion(version2, file); - - if (version2.getRevisionNumber().compareTo(version1.getRevisionNumber()) > 0) { - request.setContents(content2, content1); - request.setContentTitles(version2.getRevisionNumber().asString(), version1.getRevisionNumber().asString()); - } - else { - request.setContents(content1, content2); - request.setContentTitles(version1.getRevisionNumber().asString(), version2.getRevisionNumber().asString()); - } - - DiffManager.getInstance().getDiffTool().show(request); - } - catch (VcsException e) { - showError(e, VcsBundle.message("message.title.diff")); - } - catch (IOException e) { - showError(new VcsException(e), VcsBundle.message("message.title.diff")); - } - - } - public void showChangesBrowser(List changelists) { showChangesBrowser(changelists, null); } @@ -589,27 +544,6 @@ public class AbstractVcsHelperImpl extends AbstractVcsHelper { } } - @Nullable - public T chooseCommittedChangeList(@NotNull CommittedChangesProvider provider, - RepositoryLocation location) { - final List changes; - try { - changes = provider.getCommittedChanges(provider.createDefaultSettings(), location, 0); - } - catch (VcsException e) { - return null; - } - final ChangesBrowserDialog dlg = new ChangesBrowserDialog(myProject, new CommittedChangesTableModel((List)changes, - provider.getColumns(), false), - ChangesBrowserDialog.Mode.Choose, null); - if (dlg.showAndGet()) { - return (T)dlg.getSelectedChangeList(); - } - else { - return null; - } - } - @Override @NotNull public List showMergeDialog(@NotNull List files, @@ -622,17 +556,6 @@ public class AbstractVcsHelperImpl extends AbstractVcsHelper { return fileMergeDialog.getProcessedFiles(); } - private static DiffContent getContentForVersion(final VcsFileRevision version, final File file) throws IOException, VcsException { - VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); - if (vFile != null && (version instanceof CurrentRevision) && !vFile.getFileType().isBinary()) { - return new DocumentContent(FileDocumentManager.getInstance().getDocument(vFile), vFile.getFileType()); - } - else { - return new SimpleContent(VcsHistoryUtil.loadRevisionContentGuessEncoding(version, vFile, null), - FileTypeManager.getInstance().getFileTypeByFileName(file.getName())); - } - } - public void openCommittedChangesTab(final AbstractVcs vcs, final VirtualFile root, final ChangeBrowserSettings settings, diff --git a/platform/vcs-impl/src/com/intellij/vcs/ProgressManagerQueue.java b/platform/vcs-impl/src/com/intellij/vcs/ProgressManagerQueue.java index 9c9761ad1f70..38c67924799b 100644 --- a/platform/vcs-impl/src/com/intellij/vcs/ProgressManagerQueue.java +++ b/platform/vcs-impl/src/com/intellij/vcs/ProgressManagerQueue.java @@ -80,18 +80,9 @@ public class ProgressManagerQueue { } } - private static void runStuff(final Runnable stuff) { - try { - stuff.run(); - } - catch (ProcessCanceledException e) { - // - } - } - public void run(@NotNull final Runnable stuff) { if (ApplicationManager.getApplication().isUnitTestMode()) { - runStuff(stuff); + stuff.run(); return; } synchronized (myLock) { @@ -122,7 +113,11 @@ public class ProgressManagerQueue { } if (stuff != null) { // each task is executed only once, once it has been taken from the queue.. - runStuff(stuff); + try { + stuff.run(); + } + catch (ProcessCanceledException ignored) { + } } } catch (Throwable t) { diff --git a/platform/vcs-tests/src/com/intellij/openapi/vcs/VcsTestUtil.java b/platform/vcs-tests/src/com/intellij/openapi/vcs/VcsTestUtil.java index a31a76e73fa6..4ec3e2087605 100644 --- a/platform/vcs-tests/src/com/intellij/openapi/vcs/VcsTestUtil.java +++ b/platform/vcs-tests/src/com/intellij/openapi/vcs/VcsTestUtil.java @@ -23,6 +23,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestUtil; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -273,4 +274,7 @@ public class VcsTestUtil { return res.toString(); } + public static String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/platform/vcs-tests/testData"; + } } diff --git a/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.patch b/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.xml b/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.xml new file mode 100644 index 000000000000..926ec35db70c --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningMonth/after/test2.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningMonth/before.xml b/platform/vcs-tests/testData/shelf/cleaningMonth/before.xml new file mode 100644 index 000000000000..a9c6618c6fca --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningMonth/before.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningMonth/before/test.patch b/platform/vcs-tests/testData/shelf/cleaningMonth/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningMonth/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningMonth/before/test2.patch b/platform/vcs-tests/testData/shelf/cleaningMonth/before/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningMonth/before/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.patch b/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.xml b/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.xml new file mode 100644 index 000000000000..2f1fc1526f72 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningWeek/after/test2.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningWeek/before.xml b/platform/vcs-tests/testData/shelf/cleaningWeek/before.xml new file mode 100644 index 000000000000..28a8806c68f6 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningWeek/before.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningWeek/before/test.patch b/platform/vcs-tests/testData/shelf/cleaningWeek/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningWeek/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningWeek/before/test2.patch b/platform/vcs-tests/testData/shelf/cleaningWeek/before/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningWeek/before/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.patch b/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.xml b/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.xml new file mode 100644 index 000000000000..d3ec45ca742e --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningYear/after/test2.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningYear/before.xml b/platform/vcs-tests/testData/shelf/cleaningYear/before.xml new file mode 100644 index 000000000000..7ffa19987e5e --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningYear/before.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/cleaningYear/before/test.patch b/platform/vcs-tests/testData/shelf/cleaningYear/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningYear/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/cleaningYear/before/test2.patch b/platform/vcs-tests/testData/shelf/cleaningYear/before/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/cleaningYear/before/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.patch b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.xml b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.xml new file mode 100644 index 000000000000..61a1927d4e30 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.patch b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.xml b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.xml new file mode 100644 index 000000000000..7baf345c7c91 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/after/test2.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before.xml b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before.xml new file mode 100644 index 000000000000..3bc7b8c7f415 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test.patch b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test2.patch b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningNothingToDelete/before/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.patch b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.xml b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.xml new file mode 100644 index 000000000000..2f1fc1526f72 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/after/test2.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before.xml b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before.xml new file mode 100644 index 000000000000..28a8806c68f6 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test.patch b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test2.patch b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test2.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/defaultCleaningWhenProjectOpened/before/test2.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfo/after/test.patch b/platform/vcs-tests/testData/shelf/migrateInfo/after/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfo/after/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfo/after/test.xml b/platform/vcs-tests/testData/shelf/migrateInfo/after/test.xml new file mode 100644 index 000000000000..cfa772b5fef3 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfo/after/test.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfo/before.xml b/platform/vcs-tests/testData/shelf/migrateInfo/before.xml new file mode 100644 index 000000000000..099a6db7deb4 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfo/before.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfo/before/test.patch b/platform/vcs-tests/testData/shelf/migrateInfo/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfo/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.xml b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.xml new file mode 100644 index 000000000000..a5c4a323873e --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/after/test.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before.xml b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before.xml new file mode 100644 index 000000000000..390eb6ce7da4 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoRecycled/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/c.png b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/c.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.xml b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.xml new file mode 100644 index 000000000000..5a7b113f7191 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/after/test.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before.xml b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before.xml new file mode 100644 index 000000000000..def9213dcd2b --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before/c.png b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before/c.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithBinaries/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_.xml b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_.xml new file mode 100644 index 000000000000..0209b9407092 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/after/test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_test_.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before.xml b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before.xml new file mode 100644 index 000000000000..f1098c87c8eb --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before/test.patch b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateInfoWithVeryLongDescription/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/after/test.xml b/platform/vcs-tests/testData/shelf/migrateWithResources/after/test.xml new file mode 100644 index 000000000000..8969ab520b81 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateWithResources/after/test.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/after/test/c.png b/platform/vcs-tests/testData/shelf/migrateWithResources/after/test/c.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/after/test/shelved.patch b/platform/vcs-tests/testData/shelf/migrateWithResources/after/test/shelved.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateWithResources/after/test/shelved.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/before.xml b/platform/vcs-tests/testData/shelf/migrateWithResources/before.xml new file mode 100644 index 000000000000..e3c8749c4b58 --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateWithResources/before.xml @@ -0,0 +1,11 @@ + + + + diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/before/c.png b/platform/vcs-tests/testData/shelf/migrateWithResources/before/c.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/vcs-tests/testData/shelf/migrateWithResources/before/test.patch b/platform/vcs-tests/testData/shelf/migrateWithResources/before/test.patch new file mode 100644 index 000000000000..bb4c248ccdbd --- /dev/null +++ b/platform/vcs-tests/testData/shelf/migrateWithResources/before/test.patch @@ -0,0 +1,6 @@ +--- after/1.txt after ++++ after/1.txt after +@@ -0,0 +1,3 @@ ++One ++Two ++Three diff --git a/platform/vcs-tests/testData/vcs/directoryMappings/a-b/a-b.iml b/platform/vcs-tests/testData/vcs/directoryMappings/a-b/a-b.iml new file mode 100644 index 000000000000..9a76996fc8b9 --- /dev/null +++ b/platform/vcs-tests/testData/vcs/directoryMappings/a-b/a-b.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/platform/vcs-tests/testData/vcs/directoryMappings/a/a.iml b/platform/vcs-tests/testData/vcs/directoryMappings/a/a.iml new file mode 100644 index 000000000000..9a76996fc8b9 --- /dev/null +++ b/platform/vcs-tests/testData/vcs/directoryMappings/a/a.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/platform/vcs-tests/testData/vcs/directoryMappings/directoryMappings.ipr b/platform/vcs-tests/testData/vcs/directoryMappings/directoryMappings.ipr new file mode 100644 index 000000000000..85020d0647c7 --- /dev/null +++ b/platform/vcs-tests/testData/vcs/directoryMappings/directoryMappings.ipr @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/ShelveChangesManagerTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/ShelveChangesManagerTest.java new file mode 100644 index 000000000000..8dcdccf90418 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/ShelveChangesManagerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 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.shelf; + +import com.intellij.openapi.util.JDOMUtil; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.VcsTestUtil; +import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import org.jdom.Element; + +import java.io.File; + +public class ShelveChangesManagerTest extends PlatformTestCase { + + public void testMigrateInfo() throws Exception { + doTest(); + } + + public void testMigrateInfoRecycled() throws Exception { + doTest(); + } + + public void testMigrateInfoWithBinaries() throws Exception { + doTest(); + } + + public void testMigrateInfoWithVeryLongDescription() throws Exception { + doTest(); + } + + public void testMigrateWithResources() throws Exception { + doTest(true); + } + + private void doTest() throws Exception { + doTest(false); + } + + private void doTest(boolean migrateResources) throws Exception { + String testDataPath = VcsTestUtil.getTestDataPath() + "/shelf/" + getTestName(true); + File beforeFile = new File(testDataPath, "before"); + File afterFile = new File(testDataPath, "after"); + VirtualFile afterDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(afterFile); + File shelfFile = new File(myProject.getBasePath(), ".shelf"); + FileUtil.createDirectory(shelfFile); + myFilesToDelete.add(shelfFile); + FileUtil.copyDir(beforeFile, shelfFile); + VirtualFile shelfDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(shelfFile); + assertNotNull(shelfDir); + File beforeXmlInfo = new File(testDataPath, "before.xml"); + assert (beforeXmlInfo.exists()); + Element element = JDOMUtil.load(beforeXmlInfo); + ShelveChangesManager shelveChangesManager = ShelveChangesManager.getInstance(myProject); + shelveChangesManager.readExternal(element); + if (migrateResources) { + shelveChangesManager.checkAndMigrateOldPatchResourcesToNewSchemeStorage(); + } + shelfDir.refresh(false, true); + PlatformTestUtil.saveProject(myProject); + PlatformTestUtil.assertDirectoriesEqual(afterDir, shelfDir); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/UnshelvedChangelistsCleaningTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/UnshelvedChangelistsCleaningTest.java new file mode 100644 index 000000000000..8aabc8470177 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/shelf/UnshelvedChangelistsCleaningTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2017 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.shelf; + +import com.intellij.openapi.util.JDOMUtil; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.VcsTestUtil; +import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.util.text.DateFormatUtil; +import org.jdom.Element; + +import java.io.File; +import java.util.Calendar; +import java.util.Date; + +import static com.intellij.openapi.vcs.Executor.debug; + +public class UnshelvedChangelistsCleaningTest extends PlatformTestCase { + + private Calendar myCalendar; + private int TEST_YEAR; + + @Override + public void setUp() throws Exception { + super.setUp(); + myCalendar = Calendar.getInstance(); + TEST_YEAR = 2000; + myCalendar.set(TEST_YEAR, Calendar.JANUARY, 1); + } + + public void testDefaultCleaningWhenProjectOpened() throws Exception { + myCalendar.add(Calendar.DAY_OF_MONTH, -7); + doTest(); + } + + public void testDefaultCleaningNothingToDelete() throws Exception { + myCalendar.add(Calendar.DAY_OF_MONTH, -1); + doTest(); + } + + public void testCleaningWeek() throws Exception { + myCalendar.add(Calendar.DAY_OF_MONTH, -7); + doTest(); + } + + public void testCleaningMonth() throws Exception { + myCalendar.add(Calendar.MONTH, -1); + doTest(); + } + + public void testCleaningYear() throws Exception { + myCalendar.add(Calendar.YEAR, -1); + doTest(); + } + + private void doTest() throws Exception { + String testDataPath = VcsTestUtil.getTestDataPath() + "/shelf/" + getTestName(true); + File beforeFile = new File(testDataPath, "before"); + File afterFile = new File(testDataPath, "after"); + VirtualFile afterDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(afterFile); + assertNotNull(afterDir); + File shelfFile = new File(myProject.getBasePath(), ".shelf"); + FileUtil.createDirectory(shelfFile); + myFilesToDelete.add(shelfFile); + FileUtil.copyDir(beforeFile, shelfFile); + VirtualFile shelfDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(shelfFile); + assertNotNull(shelfDir); + File beforeXmlInfo = new File(testDataPath, "before.xml"); + assert (beforeXmlInfo.exists()); + Element element = JDOMUtil.load(beforeXmlInfo); + ShelveChangesManager shelveChangesManager = ShelveChangesManager.getInstance(myProject); + shelveChangesManager.readExternal(element); + shelfDir.refresh(false, true); + + assertFalse(shelveChangesManager.getRecycledShelvedChangeLists().isEmpty()); + Date calendarTime = myCalendar.getTime(); + String datePresentation = DateFormatUtil.formatDate(calendarTime); + assertTrue("Calendar date is: " + datePresentation, myCalendar.get(Calendar.YEAR) < TEST_YEAR); + debug(datePresentation); + shelveChangesManager.cleanUnshelved(false, myCalendar.getTimeInMillis()); + PlatformTestUtil.saveProject(myProject); + + assertFalse(shelveChangesManager.getRecycledShelvedChangeLists().isEmpty()); + shelfDir.refresh(false, true); + afterDir.refresh(false, true); + PlatformTestUtil.assertDirectoriesEqual(afterDir, shelfDir); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/AnnotationShortNameTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/AnnotationShortNameTest.java new file mode 100644 index 000000000000..eb9dfb6f1173 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/AnnotationShortNameTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.vcs.actions.ShortNameType; +import junit.framework.TestCase; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.openapi.vcs.actions.ShortNameType.FIRSTNAME; +import static com.intellij.openapi.vcs.actions.ShortNameType.LASTNAME; + +/** + * @author Konstantin Bulenkov + */ +public class AnnotationShortNameTest extends TestCase { + public void testShortNames() throws Exception { + doTest(FIRSTNAME, "Vasya Pavlovich Pupkin ", "Vasya"); + doTest(LASTNAME, "Vasya Pavlovich Pupkin ", "Pupkin"); + doTest(FIRSTNAME, "Vasya Pavlovich Pupkin", "Vasya"); + doTest(LASTNAME, "Vasya Pavlovich Pupkin", "Pupkin"); + doTest(LASTNAME, "vasya.pupkin@localhost.com", "Pupkin"); + doTest(FIRSTNAME, "vasya.pupkin@localhost.com", "Vasya"); + doTest(LASTNAME, "vasya-pavlovich-pupkin@localhost.com", "Pupkin"); + doTest(FIRSTNAME, "vasya-pavlovich-pupkin@localhost.com", "Vasya"); + doTest(FIRSTNAME, "vasya", "vasya"); + doTest(LASTNAME, "vasya", "vasya"); + doTest(FIRSTNAME, "Vasya Pupkin", "Vasya"); + doTest(LASTNAME, "Vasya Pupkin", "Pupkin"); + } + + private static void doTest(@NotNull ShortNameType type, @NotNull String fullName, @NotNull String expected) { + String actual = ShortNameType.shorten(fullName, type); + assertEquals("Type: " + type, expected, actual); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/BaseLineStatusTrackerTestCase.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/BaseLineStatusTrackerTestCase.java new file mode 100644 index 000000000000..b3f06d89eac6 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/BaseLineStatusTrackerTestCase.java @@ -0,0 +1,178 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.diff.util.DiffUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.impl.DocumentMarkupModel; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileTypes.PlainTextFileType; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.ex.LineStatusTracker; +import com.intellij.openapi.vcs.ex.LineStatusTracker.Mode; +import com.intellij.openapi.vcs.ex.Range; +import com.intellij.openapi.vcs.ex.RangesBuilder; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.LightPlatformTestCase; +import com.intellij.testFramework.LightVirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.diff.FilesTooBigForDiffException; +import org.jetbrains.annotations.NotNull; + +import java.util.BitSet; +import java.util.List; + +/** + * author: lesya + */ +public abstract class BaseLineStatusTrackerTestCase extends LightPlatformTestCase { + protected VirtualFile myFile; + protected Document myDocument; + protected Document myUpToDateDocument; + protected LineStatusTracker myTracker; + + @Override + public void tearDown() throws Exception { + try { + releaseTracker(); + } + finally { + super.tearDown(); + } + } + + protected void runCommand(@NotNull final Runnable task) { + CommandProcessor.getInstance().executeCommand(getProject(), () -> ApplicationManager.getApplication().runWriteAction(task), "", null); + } + + protected void replaceString(final int startOffset, final int endOffset, @NotNull final String s) { + runCommand(() -> myDocument.replaceString(startOffset, endOffset, s)); + } + + protected void insertString(final int offset, @NotNull final String s) { + runCommand(() -> myDocument.insertString(offset, s)); + } + + protected void deleteString(final int startOffset, final int endOffset) { + runCommand(() -> myDocument.deleteString(startOffset, endOffset)); + } + + protected void rollback(@NotNull final Range range) { + runCommand(() -> myTracker.rollbackChanges(range)); + } + + protected void rollback(@NotNull final BitSet lines) { + runCommand(() -> myTracker.rollbackChanges(lines)); + } + + protected void compareRanges() throws FilesTooBigForDiffException { + List expected = RangesBuilder.createRanges(myDocument, myUpToDateDocument); + List actual = myTracker.getRanges(); + assertEquals(expected, actual); + } + + protected void createDocument(@NotNull String text) throws FilesTooBigForDiffException { + createDocument(text, text); + compareRanges(); + assertEquals(0, DocumentMarkupModel.forDocument(myDocument, getProject(), true).getAllHighlighters().length); + } + + protected void createDocument(@NotNull String text, @NotNull final String upToDateDocument) { + createDocument(text, upToDateDocument, false); + } + + protected void createDocument(@NotNull String text, @NotNull final String upToDateDocument, boolean smart) { + myFile = new LightVirtualFile("LSTTestFile", PlainTextFileType.INSTANCE, text); + myDocument = FileDocumentManager.getInstance().getDocument(myFile); + assertNotNull(myDocument); + ApplicationManager.getApplication().runWriteAction(() -> { + assert myTracker == null; + myTracker = LineStatusTracker.createOn(myFile, myDocument, getProject(), smart ? Mode.SMART : Mode.DEFAULT); + myTracker.setBaseRevision(upToDateDocument); + }); + myUpToDateDocument = myTracker.getVcsDocument(); + } + + protected void releaseTracker() { + if (myTracker != null) { + myTracker.release(); + myTracker = null; + } + } + + protected void checkCantTrim() throws Throwable { + List ranges = myTracker.getRanges(); + for (Range range : ranges) { + if (range.getType() != Range.MODIFIED) continue; + + List lines1 = DiffUtil.getLines(myUpToDateDocument, range.getVcsLine1(), range.getVcsLine2()); + List lines2 = DiffUtil.getLines(myDocument, range.getLine1(), range.getLine2()); + + String f1 = ContainerUtil.getFirstItem(lines1); + String f2 = ContainerUtil.getFirstItem(lines2); + + String l1 = ContainerUtil.getLastItem(lines1); + String l2 = ContainerUtil.getLastItem(lines2); + + assertFalse(Comparing.equal(f1, f2)); + assertFalse(Comparing.equal(l1, l2)); + } + } + + protected void checkCantMerge() throws Throwable { + List ranges = myTracker.getRanges(); + for (int i = 0; i < ranges.size() - 1; i++) { + assertFalse(ranges.get(i).getLine2() == ranges.get(i + 1).getLine1()); + } + } + + protected void checkInnerRanges() throws Throwable { + List ranges = myTracker.getRangesInner(); + + for (Range range : ranges) { + List innerRanges = range.getInnerRanges(); + if (innerRanges == null) return; + + int last = range.getLine1(); + for (Range.InnerRange innerRange : innerRanges) { + assertEquals(innerRange.getLine1() == innerRange.getLine2(), innerRange.getType() == Range.DELETED); + + assertEquals(last, innerRange.getLine1()); + last = innerRange.getLine2(); + } + assertEquals(last, range.getLine2()); + + List lines1 = DiffUtil.getLines(myUpToDateDocument, range.getVcsLine1(), range.getVcsLine2()); + List lines2 = DiffUtil.getLines(myDocument, range.getLine1(), range.getLine2()); + + int start = 0; + for (Range.InnerRange innerRange : innerRanges) { + if (innerRange.getType() != Range.EQUAL) continue; + + for (int i = innerRange.getLine1(); i < innerRange.getLine2(); i++) { + String line = lines2.get(i - range.getLine1()); + List searchSpace = lines1.subList(start, lines1.size()); + int index = ContainerUtil.indexOf(searchSpace, (it) -> StringUtil.equalsIgnoreWhitespaces(it, line)); + assertTrue(index != -1); + start += index + 1; + } + } + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/DirectoryMappingListTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/DirectoryMappingListTest.java new file mode 100644 index 000000000000..aa0c04eb51d8 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/DirectoryMappingListTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.ide.startup.impl.StartupManagerImpl; +import com.intellij.openapi.project.ex.ProjectManagerEx; +import com.intellij.openapi.startup.StartupManager; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.actions.DescindingFilesFilter; +import com.intellij.openapi.vcs.changes.committed.MockAbstractVcs; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vcs.impl.projectlevelman.AllVcses; +import com.intellij.openapi.vcs.impl.projectlevelman.AllVcsesI; +import com.intellij.openapi.vcs.impl.projectlevelman.NewMappings; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PsiTestUtil; +import com.intellij.util.ui.UIUtil; +import com.intellij.vcsUtil.VcsUtil; +import junit.framework.Assert; +import org.jetbrains.annotations.NonNls; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +/** + * @author yole + */ +public class DirectoryMappingListTest extends PlatformTestCase { + @NonNls private static final String BASE_PATH = "/vcs/directoryMappings/"; + private NewMappings myMappings; + private VirtualFile myProjectRoot; + private String myRootPath; + private AllVcsesI myVcses; + + @Override + protected void setUpProject() throws Exception { + final String root = VcsTestUtil.getTestDataPath() + BASE_PATH; + + myProjectRoot = PsiTestUtil.createTestProjectStructure(getTestName(true),null, FileUtil.toSystemIndependentName(root), myFilesToDelete, false); + VirtualFile projectFile = myProjectRoot.findChild("directoryMappings.ipr"); + myRootPath = myProjectRoot.getPath(); + + myProject = ProjectManagerEx.getInstanceEx().loadProject(projectFile.getPath()); + ProjectManagerEx.getInstanceEx().openTestProject(myProject); + UIUtil.dispatchAllInvocationEvents(); // startup activities + + final StartupManagerImpl startupManager = (StartupManagerImpl)StartupManager.getInstance(myProject); + startupManager.runStartupActivities(); + startupManager.startCacheUpdate(); + myVcses = AllVcses.getInstance(myProject); + myVcses.registerManually(new MockAbstractVcs(myProject, "mock")); + myVcses.registerManually(new MockAbstractVcs(myProject, "CVS")); + myVcses.registerManually(new MockAbstractVcs(myProject, "mock2")); + myMappings = new NewMappings(myProject, (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(myProject), + FileStatusManager.getInstance(myProject)); + startupManager.runPostStartupActivities(); + } + + @Override + protected void tearDown() throws Exception { + myMappings.disposeMe(); + ((AllVcses) myVcses).dispose(); + + super.tearDown(); + } + + public void testMappingsFilter() throws Exception { + final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject); + ((MockAbstractVcs) vcsManager.findVcsByName("mock")).setAllowNestedRoots(true); + + final String[] pathsStr = new String[] {myRootPath + "/a", myRootPath + "/a/b", myRootPath + "/def", + myRootPath + "/a-b", myRootPath + "/a-b/d-e", myRootPath + "/a-b1/d-e"}; + final VirtualFile a = myProjectRoot.findChild("a"); + createChildDirectory(a, "b"); + createChildDirectory(myProjectRoot, "def"); + final VirtualFile ab = myProjectRoot.findChild("a-b"); + final VirtualFile ab1 = createChildDirectory(myProjectRoot, "a-b1"); + createChildDirectory(ab, "d-e"); + createChildDirectory(ab1, "d-e"); + + vcsManager.setDirectoryMappings(Arrays.asList(new VcsDirectoryMapping(pathsStr[0], "mock"), + new VcsDirectoryMapping(pathsStr[1], "mock"), + new VcsDirectoryMapping(pathsStr[2], "mock"), + new VcsDirectoryMapping(pathsStr[3], "mock2"), + new VcsDirectoryMapping(pathsStr[4], "mock2"), + new VcsDirectoryMapping(pathsStr[5], "mock2")) ); + + final FilePath[] paths = new FilePath[6]; + for (int i = 0; i < pathsStr.length; i++) { + final String s = pathsStr[i]; + paths[i] = VcsUtil.getFilePath(s, true); + + } + + assertEquals(6, vcsManager.getDirectoryMappings().size()); + final FilePath[] filePaths = DescindingFilesFilter.filterDescindingFiles(paths, myProject); + assertEquals(5, filePaths.length); + } + + public void testSamePrefix() throws Exception { + myMappings.setMapping(myRootPath + "/a", "CVS"); + myMappings.setMapping(myRootPath + "/a-b", "mock2"); + assertEquals(3, myMappings.getDirectoryMappings().size()); + myMappings.cleanupMappings(); + assertEquals(3, myMappings.getDirectoryMappings().size()); + assertEquals("mock2", myMappings.getVcsFor(myProjectRoot.findChild("a-b"))); + assertEquals("CVS", myMappings.getVcsFor(myProjectRoot.findChild("a"))); + } + + public void testSamePrefixEmpty() throws Exception { + myMappings.setMapping(myRootPath + "/a", "CVS"); + assertEquals("", myMappings.getVcsFor(myProjectRoot.findChild("a-b"))); + } + + public void testSame() throws Exception { + myMappings.removeDirectoryMapping(new VcsDirectoryMapping("", "")); + myMappings.setMapping(myRootPath + "/parent/path", "CVS"); + + final String[] children = new String[] { + myRootPath + "/parent/path", myRootPath + "\\parent\\path", myRootPath + "\\parent\\path" + }; + createFiles(children); + + for (String child : children) { + myMappings.setMapping(child, "CVS"); + myMappings.cleanupMappings(); + Assert.assertEquals("cleanup failed: " + child, 1, myMappings.getDirectoryMappings().size()); + } + + for (String child : children) { + myMappings.setMapping(child, "CVS"); + Assert.assertEquals("cleanup failed: " + child, 1, myMappings.getDirectoryMappings().size()); + } + } + + public void testHierarchy() throws Exception { + myMappings.removeDirectoryMapping(new VcsDirectoryMapping("", "")); + myMappings.setMapping(myRootPath + "/parent", "CVS"); + + final String[] children = new String[] { + myRootPath + "/parent/child1", myRootPath + "/parent/middle/child2", myRootPath + "/parent/middle/child3" + }; + createFiles(children); + + for (String child : children) { + myMappings.setMapping(child, "CVS"); + myMappings.cleanupMappings(); + Assert.assertEquals("cleanup failed: " + child, 1, myMappings.getDirectoryMappings().size()); + } + } + + public void testNestedInnerCopy() throws Exception { + myMappings.removeDirectoryMapping(new VcsDirectoryMapping("", "")); + myMappings.setMapping(myRootPath + "/parent", "CVS"); + myMappings.setMapping(myRootPath + "/parent/child", "mock"); + + final String[] children = new String[] { + myRootPath + "/parent/child1", myRootPath + "\\parent\\middle\\child2", myRootPath + "/parent/middle/child3", + myRootPath + "/parent/child/inner" + }; + createFiles(children); + + final String[] awaitedVcsNames = {"CVS","CVS","CVS","mock"}; + final LocalFileSystem lfs = LocalFileSystem.getInstance(); + for (int i = 0; i < children.length; i++) { + String child = children[i]; + final VirtualFile vf = lfs.refreshAndFindFileByIoFile(new File(child)); + Assert.assertNotNull(vf); + final VcsDirectoryMapping mapping = myMappings.getMappingFor(vf); + Assert.assertNotNull(mapping); + Assert.assertEquals(awaitedVcsNames[i], mapping.getVcs()); + } + } + + private static void createFiles(final String[] paths) throws IOException { + for (String path : paths) { + final File file = new File(FileUtil.toSystemDependentName(path)); + assert file.mkdirs() || file.isDirectory() : file; + myFilesToDelete.add(file); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ErrorMessageTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ErrorMessageTest.java new file mode 100644 index 000000000000..e7d41027e196 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ErrorMessageTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2017 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; + +import junit.framework.TestCase; +import org.jetbrains.annotations.NonNls; + +@NonNls public class ErrorMessageTest extends TestCase { + public void test() throws Exception{ + + doTest(1, 1, "One error and one warning found."); + doTest(0, 1, "No errors and one warning found."); + doTest(1, 0, "One error and no warnings found."); + doTest(1, 10, "One error and 10 warnings found."); + doTest(10, 1, "10 errors and one warning found."); + doTest(10, 0, "10 errors and no warnings found."); + doTest(0, 10, "No errors and 10 warnings found."); + } + + private void doTest(final int errors, final int warnings, final String expected) { + final String message = VcsBundle.message("before.commit.files.contain.code.smells.edit.them.confirm.text", errors, warnings); + assertTrue(message.indexOf(expected) >= 0); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/HackSearchTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/HackSearchTest.java new file mode 100644 index 000000000000..f6493791c4ac --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/HackSearchTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.vcs.checkin.HackSearch; +import com.intellij.util.containers.Convertor; +import junit.framework.Assert; +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * @author irengrig + * Date: 2/21/11 + * Time: 12:19 PM + */ +public class HackSearchTest extends TestCase { + private HackSearch mySearch; + + @Override + public void setUp() throws Exception { + super.setUp(); + mySearch = new HackSearch<>(new Convertor() { + @Override + public Z convert(T o) { + return new Z(o.getInt()); + } + }, new Convertor() { + @Override + public Z convert(S o) { + return new Z(o.getInt()); + } + }, new ZComparator()); + } + + public void testSimple() throws Exception { + final int idx = mySearch.search(Arrays.asList(new S[]{s(1), s(2), s(4), s(10)}), new T(5)); + Assert.assertEquals(3, idx); + } + + public void testSame() throws Exception { + final int idx = mySearch.search(Arrays.asList(new S[]{s(1), s(2), s(4), s(5), s(10)}), new T(5)); + Assert.assertEquals(3, idx); + } + + public void testBefore() throws Exception { + final int idx = mySearch.search(Arrays.asList(new S[]{s(10), s(20), s(40), s(50), s(60)}), new T(5)); + Assert.assertEquals(0, idx); + } + public void testFirst() throws Exception { + final int idx = mySearch.search(Arrays.asList(new S[]{s(1), s(2), s(4), s(5), s(10)}), new T(1)); + Assert.assertEquals(0, idx); + } + public void testLast() throws Exception { + final int idx = mySearch.search(Arrays.asList(new S[]{s(1), s(2), s(4), s(5), s(10)}), new T(15)); + Assert.assertEquals(5, idx); + } + + private S s(int i) { + return new S(i); + } + + private static class T { + private final int myInt; + + protected T(int anInt) { + myInt = anInt; + } + + public int getInt() { + return myInt; + } + } + + private static class S extends T { + private S(int anInt) { + super(anInt); + } + } + + private static class Z extends T { + private Z(int anInt) { + super(anInt); + } + } + + private static class ZComparator implements Comparator { + @Override + public int compare(Z o1, Z o2) { + return new Integer(o1.getInt()).compareTo(new Integer(o2.getInt())); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IgnoreIdeaLevelTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IgnoreIdeaLevelTest.java new file mode 100644 index 000000000000..d44fa8ad39a8 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IgnoreIdeaLevelTest.java @@ -0,0 +1,469 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.ide.highlighter.ModuleFileType; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleTypeId; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.changes.*; +import com.intellij.openapi.vcs.changes.committed.MockAbstractVcs; +import com.intellij.openapi.vcs.changes.ui.IgnoreUnversionedDialog; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.UsefulTestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class IgnoreIdeaLevelTest extends PlatformTestCase { + private MockAbstractVcs myVcs; + private ProjectLevelVcsManagerImpl myVcsManager; + private ChangeListManagerImpl myClManager; + private IgnoreUnversionedDialog dialog; + private ModuleManager myModuleManager; + private Module myOutsideModule; + private VirtualFile myModuleRoot; + private File myModuleRootFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myVcs = new MockAbstractVcs(getProject()); + myModuleManager = ModuleManager.getInstance(getProject()); + createOutsideModule(); + + myVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(getProject()); + myVcsManager.registerVcs(myVcs); + myVcsManager.setDirectoryMapping("", myVcs.getName()); + myVcsManager.setDirectoryMapping(myModuleRootFile.getAbsolutePath(), myVcs.getName()); + myVcsManager.updateActiveVcss(); + + myClManager = ChangeListManagerImpl.getInstanceImpl(myProject); + + dialog = new IgnoreUnversionedDialog(myProject); + Disposer.register(getTestRootDisposable(), dialog.getDisposable()); + } + + private void createOutsideModule() throws IOException { + final VirtualFile baseDir = myProject.getBaseDir(); + assertNotNull(baseDir); + final VirtualFile baseParent = baseDir.getParent(); + assertNotNull(baseParent); + myModuleRootFile = new File(baseParent.getPath().replace('/', File.separatorChar), "outside"); + final File moduleFile = new File(myModuleRootFile, "outside" + ModuleFileType.DOT_DEFAULT_EXTENSION); + try { + myModuleRootFile.mkdir(); + moduleFile.createNewFile(); + } + catch (IOException e) { + LOG.error(e); + } + + myModuleRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(myModuleRootFile); + myFilesToDelete.add(myModuleRootFile); + myFilesToDelete.add(moduleFile); + final VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(moduleFile); + ApplicationManager.getApplication().runWriteAction(() -> { + myOutsideModule = myModuleManager.newModule(virtualFile.getPath(), ModuleTypeId.JAVA_MODULE); + myOutsideModule.getModuleFile(); + }); + } + + @Override + protected void tearDown() throws Exception { + myVcsManager.unregisterVcs(myVcs); + + UsefulTestCase.clearDeclaredFields(this, IgnoreIdeaLevelTest.class); + + super.tearDown(); + } + + // translates ignored path as if it was entered into a dialog + private IgnoredFileBean translate(final IgnoredFileBean bean) { + dialog.setIgnoredFile(bean); + return dialog.getSelectedIgnoredFiles()[0]; + } + + private static class FileStructure { + private VirtualFile myABase; + private VirtualFile myBBase; + private VirtualFile myCBase; + private VirtualFile myF1Base; + private VirtualFile myF2Base; + private VirtualFile myF3Base; + private VirtualFile myF4Base; + + private VirtualFile myAOutside; + private VirtualFile myBOutside; + private VirtualFile myCOutside; + private VirtualFile myF1Outside; + private VirtualFile myF2Outside; + private VirtualFile myF3Outside; + private VirtualFile myF4Outside; + + private FileStructure(final VirtualFile baseDir, final VirtualFile outsideDir) throws Throwable { + new WriteAction() { + @Override + protected void run(@NotNull Result result) throws Throwable { + try { + myABase = baseDir.createChildDirectory(this, "a"); + myBBase = myABase.createChildDirectory(this, "b"); + myCBase = myABase.createChildDirectory(this, "c"); + myF1Base = myBBase.createChildData(this, "f1.txt"); + myF2Base = myBBase.createChildData(this, "f2.txt"); + myF3Base = myCBase.createChildData(this, "f3.txt"); + myF4Base = myCBase.createChildData(this, "f4.txt"); + + myAOutside = outsideDir.createChildDirectory(this, "a"); + myBOutside = myAOutside.createChildDirectory(this, "b"); + myCOutside = myAOutside.createChildDirectory(this, "c"); + myF1Outside = myBOutside.createChildData(this, "f1.txt"); + myF2Outside = myBOutside.createChildData(this, "f2.txt"); + myF3Outside = myCOutside.createChildData(this, "f3.txt"); + myF4Outside = myCOutside.createChildData(this, "f4.txt"); + } + catch (IOException e) { + LOG.error(e); + } + } + }.execute().throwException(); + } + } + + public void testRoots() throws Exception { + assertEquals(2, myVcsManager.getAllVcsRoots().length); + } + + public void testIgnoreBigAmountOfExactFiles() throws Exception { + File baseFile = new File(myProject.getBaseDir().getPath()); + final Set isIgnoredYes = new HashSet<>(); + final Set isIgnoredNo = new HashSet<>(); + + int N = 20000; + for (int i = 0; i < N; i++) { + File file = new File(baseFile, "tmp" + i); + file.createNewFile(); + if (i % 2 == 0) { + final IgnoredFileBean bean = translate(IgnoredBeanFactory.ignoreFile(FileUtil.toSystemIndependentName(file.getPath()), myProject)); + myClManager.addFilesToIgnore(bean); + if (i > N / 2 && isIgnoredYes.size() < 10) { + isIgnoredYes.add(file); + } + } + else if (i > N / 2 && isIgnoredNo.size() < 10) { + isIgnoredNo.add(file); + } + } + + LOG.debug("Files created"); + + final LocalFileSystem lfs = LocalFileSystem.getInstance(); + final long start = System.currentTimeMillis(); + for (File file : isIgnoredNo) { + final VirtualFile vf = lfs.refreshAndFindFileByIoFile(file); + Assert.assertNotNull(vf); + final boolean file1 = myClManager.isIgnoredFile(vf); + Assert.assertFalse(file1); + } + for (File file : isIgnoredYes) { + final VirtualFile vf = lfs.refreshAndFindFileByIoFile(file); + Assert.assertNotNull(vf); + final boolean file1 = myClManager.isIgnoredFile(vf); + Assert.assertTrue(file1); + } + final long end = System.currentTimeMillis(); + LOG.debug("Millis passed: " + (end - start)); + } + + @Test + public void testSimple() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreFile(fs.myF1Base.getPath(), myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile(fs.myF2Outside.getPath(), myProject); + + final IgnoredFileBean translated1 = translate(bean1); + final IgnoredFileBean translated2 = translate(bean2); + + myClManager.setFilesToIgnore(translated1, translated2); + + printIgnored(); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF2Outside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Base)); + } + + @Test + public void testSimpleStrasse() throws Throwable { + final VirtualFile file = createFileInCommand(myProject.getBaseDir(), "Straße", "123"); + final VirtualFile file2 = createFileInCommand(myProject.getBaseDir(), "Strasse", "123"); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreFile(file.getPath(), myProject); + + final IgnoredFileBean translated1 = translate(bean1); + + myClManager.setFilesToIgnore(translated1); + + printIgnored(); + Assert.assertTrue(myClManager.isIgnoredFile(file)); + Assert.assertFalse(myClManager.isIgnoredFile(file2)); + } + + @Test + public void testPatterns() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.withMask("*1.txt"); + final IgnoredFileBean translated1 = translate(bean1); + myClManager.setFilesToIgnore(translated1); + + printIgnored(); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Outside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Base)); + } + + @Test + public void testDirs() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory(fs.myBBase.getPath(), myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreUnderDirectory(fs.myBOutside.getPath(), myProject); + final IgnoredFileBean translated1 = translate(bean1); + final IgnoredFileBean translated2 = translate(bean2); + + myClManager.setFilesToIgnore(translated1, translated2); + + printIgnored(); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF2Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Base)); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF2Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBOutside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCOutside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF3Outside)); + } + + @Test + public void testTypedAbsolute() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + String baseUnder = myProject.getBaseDir().getPath(); + if (!baseUnder.endsWith("/")) { + baseUnder += "/"; + } + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory(baseUnder + "a/b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile(baseUnder + "a/c/f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBBase)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Base)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + } + + @Test + public void testTypedAbsoluteSeparator() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + String baseUnder = myProject.getBaseDir().getPath(); + if (!baseUnder.endsWith("/")) { + baseUnder += "/"; + } + baseUnder = baseUnder.replace('/', File.separatorChar); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory(baseUnder + "a" + File.separator + "b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile(baseUnder + "a" + File.separator + "c" + File.separator + "f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBBase)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Base)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + } + + @Test + public void testTypedRelativeInsideSeparator() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory("a" + File.separator + "b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile("a" + File.separator + "c" + File.separator + "f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBBase)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Base)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + } + + @Test + public void testTypedRelativeInside() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory("a/b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile("a/c/f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBBase)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Base)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Base)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCBase)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + } + + @Test + public void testTypedRelativeOutsideSeparator() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + String baseUnder = myModuleRoot.getPath(); + if (!baseUnder.endsWith("/")) { + baseUnder += "/"; + } + baseUnder = baseUnder.replace('/', File.separatorChar); + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory(baseUnder + "a" + File.separator + "b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile(baseUnder + "a" + File.separator + "c" + File.separator + "f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBOutside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Outside)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCOutside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + } + + @Test + public void testTypedRelativeOutside() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + + String baseUnder = myModuleRoot.getPath(); + if (!baseUnder.endsWith("/")) { + baseUnder += "/"; + } + + final IgnoredFileBean bean1 = IgnoredBeanFactory.ignoreUnderDirectory(baseUnder + "a/b", myProject); + final IgnoredFileBean bean2 = IgnoredBeanFactory.ignoreFile(baseUnder + "a/c/f3.txt", myProject); + + myClManager.setFilesToIgnore(bean1, bean2); + + Assert.assertTrue(myClManager.isIgnoredFile(fs.myBOutside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF1Outside)); + Assert.assertTrue(myClManager.isIgnoredFile(fs.myF3Outside)); + + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Outside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myCOutside)); + Assert.assertFalse(myClManager.isIgnoredFile(fs.myF4Base)); + } + + @Test + public void testDoNotAddAlreadyIgnoredDirectory() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + myClManager.addDirectoryToIgnoreImplicitly(fs.myABase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myBBase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myCBase.getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myABase); + } + + @Test + public void testRemoveChildIgnoredDirectoryWhenParentIsAdded() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + myClManager.addDirectoryToIgnoreImplicitly(fs.myBBase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myCBase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myABase.getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myABase); + } + + @Test + public void testManuallyRemovedFromIgnored() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + myClManager.getIgnoredFilesComponent().setDirectoriesManuallyRemovedFromIgnored(Collections.singleton(fs.myBBase.getPath())); + myClManager.addDirectoryToIgnoreImplicitly(fs.myBBase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myCBase.getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myCBase); + } + + @Test + public void testRemovingFromImplicitlyIgnored() throws Throwable { + final FileStructure fs = new FileStructure(myProject.getBaseDir(), myModuleRoot); + myClManager.addDirectoryToIgnoreImplicitly(fs.myBBase.getPath()); + myClManager.addDirectoryToIgnoreImplicitly(fs.myCBase.getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myCBase, fs.myBBase); + + myClManager.removeImplicitlyIgnoredDirectory(fs.myCBase.getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myBBase); + + // removing parent directory exclude should not affect child excludes + myClManager.removeImplicitlyIgnoredDirectory(fs.myBBase.getParent().getPath()); + ConvertExcludedToIgnoredTest.assertIgnoredDirectories(myProject, fs.myBBase); + } + + private void printIgnored() { + final IgnoredFileBean[] filesToIgnore = myClManager.getFilesToIgnore(); + LOG.debug("Ignored:"); + for (IgnoredFileBean bean : filesToIgnore) { + if (IgnoreSettingsType.MASK.equals(bean.getType())) { + LOG.debug(bean.getMask()); + } + else { + LOG.debug(bean.getPath()); + } + } + } + + public VirtualFile createFileInCommand(final VirtualFile parent, final String name, @Nullable final String content) { + return VcsTestUtil.createFile(myProject, parent, name, content); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IntersectionTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IntersectionTest.java new file mode 100644 index 000000000000..36c9de2b0fe7 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/IntersectionTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vcs.checkin.StepIntersection; +import com.intellij.util.containers.Convertor; +import junit.framework.Assert; +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.List; + +/** + * @author irengrig + * Date: 2/18/11 + * Time: 9:25 AM + */ +public class IntersectionTest extends TestCase { + public void testSimple() { + final Data second = new Data("second", 20, 21); + final Data third = new Data("third", 22, 30); + final Data[] data = {new Data("first", 10,19), second, third}; + final Area[] areas = {new Area("Afirst", 1,1), new Area("Asecond", 2,2), new Area("Athird", 21,21)}; + final StepIntersection intersection = createIntersection(areas); + final List result = intersection.process(Arrays.asList(data)); + Assert.assertTrue(result.size() == 1); + Assert.assertTrue(result.contains(second)); + } + + public void testAllBefore() { + final Data second = new Data("second", 20, 21); + final Data third = new Data("third", 22, 30); + final Data[] data = {new Data("first", 10,19), second, third}; + final Area[] areas = {new Area("Afirst", 100,100), new Area("Asecond", 101,102), new Area("Athird", 210,210)}; + final StepIntersection intersection = createIntersection(areas); + final List result = intersection.process(Arrays.asList(data)); + Assert.assertTrue(result.size() == 0); + } + + public void testAllAfter() { + final Data second = new Data("second", 20, 21); + final Data third = new Data("third", 22, 30); + final Data[] data = {new Data("first", 10,19), second, third}; + final Area[] areas = {new Area("Afirst", 1,1), new Area("Asecond", 2,2), new Area("Athird", 3,3)}; + final StepIntersection intersection = createIntersection(areas); + final List result = intersection.process(Arrays.asList(data)); + Assert.assertTrue(result.size() == 0); + } + + public void testChangeIterators() { + final Data first = new Data("first", 10, 20); + final Data fourth = new Data("fourth", 70, 80); + final Data[] data = {first, new Data("second", 30,40), new Data("third", 50,60), fourth, new Data("fifth", 90,100)}; + final Area[] areas = {new Area("Afirst", 1,1), new Area("Asecond", 11,12), new Area("Athird", 21,21), + new Area("Afourth", 41,41), new Area("Afifth", 61,61), new Area("Asixth", 71,71)}; + final StepIntersection intersection = createIntersection(areas); + final List result = intersection.process(Arrays.asList(data)); + Assert.assertTrue(result.size() == 2); + Assert.assertTrue(result.contains(first)); + Assert.assertTrue(result.contains(fourth)); + } + + public void testAreasOneAfterAnother() { + final Data first = new Data("first", 77, 87); + final Data fourth = new Data("fourth", 140, 158); + final Data third = new Data("third", 225, 238); + final Data fifth = new Data("fifth", 449, 456); + final Data[] data = {first, fourth, third, fifth}; + final Area[] areas = {new Area("Afirst", 0,204), new Area("Asecond", 205,238), new Area("Athird", 239,457)}; + final StepIntersection intersection = createIntersection(areas); + final List result = intersection.process(Arrays.asList(data)); + Assert.assertEquals(4, result.size()); + Assert.assertTrue(result.contains(third)); + Assert.assertTrue(result.contains(first)); + Assert.assertTrue(result.contains(fourth)); + Assert.assertTrue(result.contains(fifth)); + } + + private StepIntersection createIntersection(Area[] areas) { + return new StepIntersection<>(new Convertor() { + @Override + public TextRange convert(Data o) { + return o.getTextRange(); + } + }, new Convertor() { + @Override + public TextRange convert(Area o) { + return o.getTextRange(); + } + }, Arrays.asList(areas)); + } + + private static class Data { + private final String myName; + private final int myFirst; + private final int mySecond; + + protected Data(String name, int first, int second) { + myName = name; + myFirst = first; + mySecond = second; + } + + public TextRange getTextRange() { + return new TextRange(myFirst, mySecond); + } + } + + private static class Area extends Data{ + private Area(String name, int first, int second) { + super(name, first, second); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LineStatusTrackerRevertAutoTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LineStatusTrackerRevertAutoTest.java new file mode 100644 index 000000000000..ed269c89b94f --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LineStatusTrackerRevertAutoTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.vcs.ex.Range; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; +import java.util.BitSet; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +public class LineStatusTrackerRevertAutoTest extends BaseLineStatusTrackerTestCase { + private static final Logger LOG = Logger.getInstance(LineStatusTrackerRevertAutoTest.class); + private Random myRng; + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + public void testSimple() throws Throwable { + doTest(System.currentTimeMillis(), 100, 10, 30, 10, -1, false); + } + + public void testComplex() throws Throwable { + doTest(System.currentTimeMillis(), 100, 10, 30, 10, 5, false); + } + + public void testInitial() throws Throwable { + doTestInitial(System.currentTimeMillis(), 100, 10, false); + } + + public void testSimpleSmart() throws Throwable { + doTest(System.currentTimeMillis(), 100, 10, 30, 10, -1, true); + } + + public void testComplexSmart() throws Throwable { + doTest(System.currentTimeMillis(), 100, 10, 30, 10, 5, true); + } + + public void testInitialSmart() throws Throwable { + doTestInitial(System.currentTimeMillis(), 100, 10, true); + } + + public void doTest(long seed, int testRuns, int modifications, int testLength, final int changeLength, int iterations, boolean smart) + throws Throwable { + myRng = new Random(seed); + for (int i = 0; i < testRuns; i++) { + long currentSeed = getCurrentSeed(); + if (i % 1000 == 0) LOG.debug(String.valueOf(i)); + try { + String initial = generateText(testLength); + createDocument(initial, initial, smart); + //System.out.println("Initial: " + initial.replace("\n", "\\n")); + + int count = myRng.nextInt(modifications); + for (int j = 0; j < count; j++) { + final int writeChanges = myRng.nextInt(4) + 1; + runCommand(() -> { + for (int k = 0; k < writeChanges; k++) { + applyRandomChange(changeLength); + } + }); + + checkCantTrim(); + checkCantMerge(); + checkInnerRanges(); + } + + if (iterations > 0) { + checkRevertComplex(iterations); + } + else { + checkRevert(myTracker.getRanges().size() * 2); + } + + releaseTracker(); + UIUtil.dispatchAllInvocationEvents(); + } + catch (Throwable e) { + System.out.println("Seed: " + seed); + System.out.println("TestRuns: " + testRuns); + System.out.println("Modifications: " + modifications); + System.out.println("TestLength: " + testLength); + System.out.println("ChangeLength: " + changeLength); + System.out.println("I: " + i); + System.out.println("Current seed: " + currentSeed); + throw e; + } + } + } + + public void doTestInitial(long seed, int testRuns, int testLength, boolean smart) throws Throwable { + myRng = new Random(seed); + for (int i = 0; i < testRuns; i++) { + if (i % 1000 == 0) LOG.debug(String.valueOf(i)); + long currentSeed = getCurrentSeed(); + try { + String initial = generateText(testLength); + String initialVcs = generateText(testLength); + createDocument(initial, initialVcs, smart); + + checkCantTrim(); + checkCantMerge(); + checkInnerRanges(); + + checkRevert(myTracker.getRanges().size() * 2); + + releaseTracker(); + UIUtil.dispatchAllInvocationEvents(); + } + catch (Throwable e) { + System.out.println("Seed: " + seed); + System.out.println("TestRuns: " + testRuns); + System.out.println("TestLength: " + testLength); + System.out.println("I: " + i); + System.out.println("Current seed: " + currentSeed); + throw e; + } + } + } + + private void checkRevert(int maxIterations) throws Exception { + int count = 0; + while (true) { + if (count > maxIterations) throw new Exception("Revert loop detected"); + List ranges = myTracker.getRanges(); + if (ranges.isEmpty()) break; + int index = myRng.nextInt(ranges.size()); + Range range = ranges.get(index); + + rollback(range); + count++; + } + assertEquals(myDocument.getText(), myUpToDateDocument.getText()); + } + + private void checkRevertComplex(int iterations) throws Exception { + BitSet lines = new BitSet(); + + for (int i = 0; i < iterations; i++) { + lines.clear(); + + for (int j = 0; j < myDocument.getLineCount() + 2; j++) { + if (myRng.nextInt(10) < 3) { + lines.set(j); + } + } + + rollback(lines); + } + + lines.set(0, myDocument.getLineCount() + 2); + rollback(lines); + + assertEquals(myDocument.getText(), myUpToDateDocument.getText()); + } + + private void applyRandomChange(int changeLength) { + int textLength = myDocument.getTextLength(); + int type = myRng.nextInt(3); + int offset = textLength != 0 ? myRng.nextInt(textLength) : 0; + int length = textLength - offset != 0 ? myRng.nextInt(textLength - offset) : offset; + String data = generateText(changeLength); + //System.out.println("Change: " + type + " - " + offset + " - " + length + " - " + data.replace("\n", "\\n")); + switch (type) { + case 0: // insert + myDocument.insertString(offset, data); + break; + case 1: // delete + myDocument.deleteString(offset, offset + length); + break; + case 2: // modify + myDocument.replaceString(offset, offset + length, data); + break; + } + } + + @NotNull + private String generateText(int textLength) { + int length = myRng.nextInt(textLength); + StringBuilder builder = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + int rnd = myRng.nextInt(10); + if (rnd == 0) { + builder.append(' '); + } + else if (rnd < 7) { + builder.append(String.valueOf(rnd)); + } + else { + builder.append('\n'); + } + } + + return builder.toString(); + } + + private long getCurrentSeed() throws Exception { + Field seedField = myRng.getClass().getDeclaredField("seed"); + seedField.setAccessible(true); + AtomicLong seedFieldValue = (AtomicLong) seedField.get(myRng); + return seedFieldValue.get() ^ 0x5DEECE66DL; + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LinesForDocumentTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LinesForDocumentTest.java new file mode 100644 index 000000000000..121a14b2f21e --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/LinesForDocumentTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.diff.util.DiffUtil; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.testFramework.PlatformTestCase; + +import java.util.Arrays; + +/** + * author: lesya + */ + + +public class LinesForDocumentTest extends PlatformTestCase { + public void test() { + doTest("", new String[]{""}); + doTest(" ", new String[]{" "}); + doTest("\n", new String[]{"", + ""}); + doTest("\na\n", new String[]{"", + "a", + ""}); + doTest("\na", new String[]{"", + "a"}); + doTest("a\n\nb", new String[]{"a", + "", + "b"}); + doTest("ab\ncd", new String[]{"ab", + "cd"}); + doTest("ab\ncd\n", new String[]{"ab", + "cd", + ""}); + doTest("\nab\ncd", new String[]{"", + "ab", + "cd"}); + doTest("\nab\ncd\n", new String[]{"", + "ab", + "cd", + ""}); + } + + private static void doTest(String text, String[] expectedLines) { + Document document = EditorFactory.getInstance().createDocument(text); + assertEquals(Arrays.asList(expectedLines), DiffUtil.getLines(document)); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ModifyDocumentTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ModifyDocumentTest.java new file mode 100644 index 000000000000..6fab4ec1dc25 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/ModifyDocumentTest.java @@ -0,0 +1,474 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.diff.DiffManager; +import com.intellij.openapi.editor.impl.DocumentImpl; +import com.intellij.openapi.editor.markup.MarkupEditorFilterFactory; +import com.intellij.openapi.vcs.ex.Range; +import com.intellij.util.diff.FilesTooBigForDiffException; + +import java.util.Arrays; +import java.util.BitSet; + + +/** + * author: lesya + */ +public class ModifyDocumentTest extends BaseLineStatusTrackerTestCase { + + public void testInit() { + DiffManager instance = DiffManager.getInstance(); + assertNotNull(instance); + MarkupEditorFilterFactory.createNotFilter(instance.getDiffEditorFilter()); + MarkupEditorFilterFactory.createIsNotDiffFilter(); + } + + public void testSimpleInsert() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(2, "a"); + compareRanges(); + } + + public void testUndo() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(7, "a"); + compareRanges(); + deleteString(7, 8); + compareRanges(); + } + + public void testLineEndBeforeModification() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(6, "a"); + compareRanges(); + insertString(5, "\n"); + compareRanges(); + } + + public void testLineEndBeforeModification2() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(6, "a"); + compareRanges(); + insertString(4, "\n"); + compareRanges(); + } + + public void testInsertDoubleEnterAtEnd() throws Throwable { + createDocument("1"); + insertString(1, "\n"); + compareRanges(); + insertString(2, "\n"); + compareRanges(); + } + + public void testSimpleInsertAndWholeReplace() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(2, "a"); + compareRanges(); + replaceString(0, myDocument.getTextLength(), " "); + compareRanges(); + } + + public void testSimpleInsert2() throws Throwable { + createDocument("1\n2\n3\n4\n5"); + insertString(4, "a"); + compareRanges(); + } + + public void testSimpleInsertToEmpty() throws Throwable { + createDocument(""); + insertString(0, "a"); + compareRanges(); + } + + public void testDoubleSimpleInsert() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(2, "a"); + compareRanges(); + insertString(2, "a"); + compareRanges(); + } + + public void testInsertEnter() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(2, "\n"); + compareRanges(); + } + + public void testSimpleInsertAndEnterToEmpty() throws Throwable { + createDocument(""); + insertString(0, "a"); + compareRanges(); + insertString(1, "\n"); + compareRanges(); + } + + public void testInsertEnterAtEnter() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(4, "\n"); + compareRanges(); + } + + public void testInsertEnterAtEnterAndSimpleInsert() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(4, "\n"); + compareRanges(); + insertString(5, "a"); + compareRanges(); + } + + public void testInsertDoubleEnterAtEnters() throws Throwable { + createDocument("1234\n2345\n3456"); + insertString(4, "\n"); + compareRanges(); + insertString(10, "\n"); + compareRanges(); + + } + + public void testInsertEnterAndSpaceAfterEnter() throws Throwable { + createDocument("12345\n12345\n12345"); + insertString(5, "\n "); + compareRanges(); + } + + public void testInsertEnterAndDeleteEnter1() throws Throwable { + createDocument("12345\n12345\n12345"); + insertString(5, "\n"); + compareRanges(); + deleteString(5, 6); + compareRanges(); + } + + public void testInsertEnterAndDeleteEnter2() throws Throwable { + createDocument("12345\n12345\n12345"); + insertString(5, "\n"); + compareRanges(); + deleteString(6, 7); + compareRanges(); + } + + public void testSimpleDelete() throws Throwable { + createDocument("1234\n2345\n3456"); + deleteString(2, 3); + compareRanges(); + } + + public void testDeleteLine() throws Throwable { + createDocument("1234\n2345\n3456"); + deleteString(0, 5); + compareRanges(); + } + + public void testDoubleDelete() throws Throwable { + createDocument("1234\n2345\n3456"); + deleteString(2, 3); + deleteString(2, 3); + compareRanges(); + } + + public void testDeleteEnter() throws Throwable { + createDocument("12345\n23456\n34567"); + deleteString(5, 6); + compareRanges(); + } + + public void testDeleteDoubleEnter() throws Throwable { + createDocument("12345\n\n23456\n34567"); + deleteString(5, 6); + compareRanges(); + } + + // + public void testDoubleInsertToClass() throws Throwable { + createDocument("class A{\n\n}"); + insertString(9, "a"); + compareRanges(); + insertString(10, "a"); + compareRanges(); + } + + public void testInsertSymbolAndEnterToClass() throws Throwable { + createDocument("class A{\n\n}"); + insertString(9, "a"); + compareRanges(); + insertString(10, "\n"); + compareRanges(); + } + + public void testMultiLineReplace2() throws Throwable { + createDocument("012a\n012b\n012c"); + replaceString(4, 9, "\nx\ny\nz"); + compareRanges(); + } + + public void testChangedLines1() throws Throwable { + createDocument("class A{\nx\na\nb\nc\n}", "class A{\n1\nx\n2\n}"); + compareRanges(); + } + + public void testMultiLineReplace1() throws Throwable { + createDocument("012a\n012b\n012c\n012d\n012e"); + replaceString(5, 6, "a"); + compareRanges(); + replaceString(4, 14, "\nx"); + compareRanges(); + } + + public void testInsertAndModify() throws FilesTooBigForDiffException { + createDocument("a\nb\nc\nd"); + insertString(3, "\n"); + compareRanges(); + insertString(4, "\n"); + compareRanges(); + insertString(6, " "); + compareRanges(); + } + + public void testRangesShouldMerge() throws FilesTooBigForDiffException { + createDocument("1\n2\n3\n4"); + insertString(1, "1"); + compareRanges(); + insertString(6, "3"); + compareRanges(); + insertString(3, "2"); + compareRanges(); + } + + public void testShiftRangesAfterChange() throws FilesTooBigForDiffException { + createDocument("1\n2\n3\n4"); + insertString(7, "4"); + compareRanges(); + insertString(0, "\n"); + compareRanges(); + insertString(0, "\n"); + compareRanges(); + insertString(0, "\n"); + compareRanges(); + } + + public void testInsertBeforeChange() throws FilesTooBigForDiffException { + createDocument(" 11\n 3 \n 44\n 55\n 6\n 7\n 88\n ", " 1\n 2\n 3 \n 4\n 5\n 6\n 7\n 8\n "); + insertString(9, "3"); + compareRanges(); + assertEquals(" 11\n 33 \n 44\n 55\n 6\n 7\n 88\n ", myDocument.getText()); + insertString(9, "aaa\nbbbbbbbb\ncccc\ndddd"); + compareRanges(); + } + + + public void testUndoDeletion() throws FilesTooBigForDiffException { + createDocument("1\n2\n3\n4\n5\n6\n7\n"); + deleteString(4, 6); + assertEquals("1\n2\n4\n5\n6\n7\n", myDocument.getText()); + compareRanges(); + insertString(4, "3\n"); + compareRanges(); + } + + public void testUndoDeletion2() throws FilesTooBigForDiffException { + createDocument("1\n2\n3\n4\n5\n6\n7\n"); + deleteString(3, 5); + assertEquals("1\n2\n4\n5\n6\n7\n", myDocument.getText()); + compareRanges(); + insertString(4, "\n3"); + compareRanges(); + } + + public void testSRC17123() throws FilesTooBigForDiffException { + createDocument("package package;\n" + "\n" + "public class Class3 {\n" + " public int i1;\n" + " public int i2;\n" + + " public int i3;\n" + " public int i4;\n" + "\n" + " public static void main(String[] args) {\n" + "\n" + + " }\n" + "}"); + deleteString(39, 58); + compareRanges(); + assertEquals("package package;\n" + "\n" + "public class Class3 {\n" + " public int i2;\n" + " public int i3;\n" + + " public int i4;\n" + "\n" + " public static void main(String[] args) {\n" + "\n" + " }\n" + "}", + myDocument.getText()); + + deleteString(39, myDocument.getTextLength()); + compareRanges(); + deleteString(myDocument.getTextLength() - 1, myDocument.getTextLength()); + } + + public void testUnexpetedDeletedRange() throws FilesTooBigForDiffException { + createDocument(" public class\n bbb\n"); + insertString(17, " \n"); + assertEquals(" public class\n \n bbb\n", myDocument.getText()); + compareRanges(); + deleteString(17, 21); + assertEquals(" public class\n\n bbb\n", myDocument.getText()); + compareRanges(); + insertString(18, " \n"); + assertEquals(" public class\n\n \n bbb\n", myDocument.getText()); + compareRanges(); + deleteString(18, 22); + assertEquals(" public class\n\n\n bbb\n", myDocument.getText()); + compareRanges(); + deleteString(4, 10); + assertEquals(" class\n\n\n bbb\n", myDocument.getText()); + compareRanges(); + insertString(4, "p"); + assertEquals(" p class\n\n\n bbb\n", myDocument.getText()); + compareRanges(); + insertString(5, "r"); + assertEquals(" pr class\n\n\n bbb\n", myDocument.getText()); + compareRanges(); + insertString(6, "i"); + assertEquals(" pri class\n\n\n bbb\n", myDocument.getText()); + compareRanges(); + } + + public void testSrc29814() throws FilesTooBigForDiffException { + String text = "111\n" + "222\n" + "333\n"; + + createDocument(text); + deleteString(0, text.length()); + compareRanges(); + assertEquals("", myDocument.getText()); + insertString(0, "222\n"); + compareRanges(); + deleteString(0, 4); + compareRanges(); + insertString(0, text); + compareRanges(); + } + + public void testDeletingTwoMethods() throws FilesTooBigForDiffException { + + String part1 = "class Foo {\n" + " public void method1() {\n" + " // something\n" + " }\n" + "\n"; + + String part2 = " public void method2() {\n" + " // something\n" + " }\n" + "\n" + " public void method3() {\n" + + " // something\n" + " }\n"; + + String part3 = "\n" + " public void method4() {\n" + " // something\n" + " }\n" + "}"; + + String text = part1 + part2 + part3; + + createDocument(text); + deleteString(part1.length(), part1.length() + part2.length()); + assertEquals(part1 + part3, myDocument.getText()); + assertEquals(Arrays.asList(new Range(5, 5, 5, 12)), myTracker.getRanges()); + + deleteString(part1.length(), part1.length() + 1); + compareRanges(); + } + + public void testBug1() throws Throwable { + createDocument("1\n2\n3\n4\n"); + deleteString(4, 6); + compareRanges(); + insertString(3, "X"); + compareRanges(); + } + + public void testBug2() throws Throwable { + createDocument("1\n2\n3\n4\n5\n6\n"); + deleteString(4, 6); + compareRanges(); + deleteString(8, 10); + compareRanges(); + insertString(4, "3\n8\n"); + compareRanges(); + } + + public void testBug3() throws Throwable { + createDocument("\n\n00\n556\n"); + + deleteString(3, 6); + checkCantTrim(); + deleteString(1, 4); + checkCantTrim(); + deleteString(0, 2); + checkCantTrim(); + insertString(0, "\n\n32\n"); + checkCantTrim(); + deleteString(1, 4); + checkCantTrim(); + } + + public void testBug4() throws Throwable { + createDocument("\n5\n30\n5240\n32\n46\n\n\n\n51530\n\n"); + + insertString(3, "40\n1\n2"); + checkCantTrim(); + deleteString(10, 25); + checkCantTrim(); + deleteString(1, 5); + checkCantTrim(); + insertString(9, "30\n\n23"); + checkCantTrim(); + deleteString(2, 11); + checkCantTrim(); + } + + public void testBug5() throws Throwable { + createDocument("\n"); + + replaceString(0, 0, "\n\n6406"); + deleteString(1, 2); + insertString(1, "\n11\n5"); + insertString(3, "130"); + replaceString(8, 8, "3"); + replaceString(9, 14, "4\n\n56\n21\n"); + replaceString(3, 17, " 60246"); + insertString(7, "01511"); + insertString(9, "2633\n33"); + deleteString(16, 17); + deleteString(15, 19); + deleteString(4, 15); + replaceString(2, 3, "\n34\n\n310\n"); + deleteString(2, 3); + deleteString(8, 10); + insertString(1, "051"); + checkCantTrim(); + } + + public void testTrimSpaces1() throws Throwable { + createDocument("a \nb \nc "); + insertString(0, "x"); + ((DocumentImpl)myDocument).stripTrailingSpaces(null, true); + + BitSet lines = new BitSet(); + lines.set(0); + rollback(lines); + + ((DocumentImpl)myDocument).stripTrailingSpaces(null, true); + assertEquals("a \nb \nc ", myDocument.getText()); + } + + public void testTrimSpaces2() throws Throwable { + createDocument("a \nb \nc "); + insertString(0, "x"); + ((DocumentImpl)myDocument).stripTrailingSpaces(null, true); + + assertEquals("xa\nb \nc ", myDocument.getText()); + } + + public void testTrimSpaces3() throws Throwable { + createDocument("a \nb \nc "); + insertString(6, "x"); + insertString(0, "x"); + ((DocumentImpl)myDocument).stripTrailingSpaces(null, true); + + BitSet lines = new BitSet(); + lines.set(2); + rollback(lines); + + ((DocumentImpl)myDocument).stripTrailingSpaces(null, true); + assertEquals("xa\nb \nc ", myDocument.getText()); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchAutoInitTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchAutoInitTest.java new file mode 100644 index 000000000000..c4d2489062b6 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchAutoInitTest.java @@ -0,0 +1,576 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.diff.impl.patch.PatchHunk; +import com.intellij.openapi.diff.impl.patch.PatchReader; +import com.intellij.openapi.diff.impl.patch.TextFilePatch; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.changes.patch.AbstractFilePatchInProgress; +import com.intellij.openapi.vcs.changes.patch.MatchPatchPaths; +import com.intellij.openapi.vcs.changes.shelf.ShelvedBinaryFile; +import com.intellij.openapi.vcs.changes.shelf.ShelvedBinaryFilePatch; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PsiTestUtil; +import com.intellij.testFramework.VfsTestUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@PlatformTestCase.WrapInCommand +public class PatchAutoInitTest extends PlatformTestCase { + private static final String BINARY_FILENAME = "binary.png"; + + public void testSimple() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + final VirtualFile dir = createChildDirectory(root, "dir"); + createChildData(dir, "somefile.txt"); + + final TextFilePatch patch = create("dir/somefile.txt"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List filePatchInProgresses = iterator.execute(Collections.singletonList(patch)); + + assertEquals(1, filePatchInProgresses.size()); + assertEquals(root, filePatchInProgresses.get(0).getBase()); + assertEquals("dir/somefile.txt", filePatchInProgresses.get(0).getCurrentPath()); + assertEquals(0, filePatchInProgresses.get(0).getCurrentStrip()); + + FileUtil.delete(new File(dir.getPath())); + } + + static TextFilePatch create(String s) { + final TextFilePatch patch = new TextFilePatch(null); + patch.setBeforeName(s); + patch.setAfterName(s); + return patch; + } + + private static TextFilePatch createFileAddition(String filename) { + TextFilePatch patch = create(filename); + patch.addHunk(new PatchHunk(-1, -1, 1, 1)); + return patch; + } + + private static TextFilePatch createFileDeletion(String filename) { + TextFilePatch patch = create(filename); + patch.addHunk(new PatchHunk(1, 1, -1, -1)); + return patch; + } + + // 1. several files with different bases; one can be matched to 2 bases + public void testDiffBases() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + createChildData(c, "f1.txt"); + createChildData(c, "f2.txt"); + createChildData(c, "f3.txt"); + createChildData(c, "f4.txt"); + createChildData(c, BINARY_FILENAME); + + VirtualFile e = createChildDirectory(root, "e"); + VirtualFile f = createChildDirectory(e, "b"); + VirtualFile g = createChildDirectory(f, "c"); + createChildData(g, "f1.txt"); + createChildData(g, "f2.txt"); + createChildData(g, "f3.txt"); + createChildData(g, "f4.txt"); + createChildData(g, BINARY_FILENAME); + + TextFilePatch patch1 = create("b/c/f1.txt"); + TextFilePatch patch2 = create("a/b/c/f2.txt"); + TextFilePatch patch3 = create("e/b/c/f3.txt"); + TextFilePatch patch4 = create("c/f4.txt"); + ShelvedBinaryFilePatch shelvedBinaryPatch = createShelvedBinarySimplePatch("c/" + BINARY_FILENAME); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Arrays.asList(patch1, patch2, patch3, patch4, shelvedBinaryPatch)); + checkPath(result, "b/c/f1.txt", Arrays.asList(a, e), 0); + checkPath(result, "a/b/c/f2.txt", Collections.singletonList(root), 0); + checkPath(result, "e/b/c/f3.txt", Collections.singletonList(root), 0); + checkPath(result, "c/f4.txt", Arrays.asList(b, f), 0); + checkPath(result, "c/" + BINARY_FILENAME, Arrays.asList(b, f), 0); + } + + public void testBestBinaryVariant() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + VfsTestUtil.createFile(c, BINARY_FILENAME); + VirtualFile cc = createChildDirectory(root, "c"); + VfsTestUtil.createFile(cc, BINARY_FILENAME); + VirtualFile e = createChildDirectory(root, "e"); + VirtualFile f = createChildDirectory(e, "b"); + VirtualFile g = createChildDirectory(f, "c"); + VfsTestUtil.createFile(g, BINARY_FILENAME); + + String cBinary = "c/" + BINARY_FILENAME; + + ShelvedBinaryFilePatch shelvedBinaryPatch = createShelvedBinarySimplePatch(cBinary); + final MatchPatchPaths matchPatchPaths = new MatchPatchPaths(myProject); + final List resultProjectBase = matchPatchPaths.execute(Collections.singletonList(shelvedBinaryPatch)); + checkPath(resultProjectBase, cBinary, Arrays.asList(root, b, f), 0); + assertEquals(resultProjectBase.get(0).getBase(), myProject.getBaseDir()); + } + + @NotNull + private static ShelvedBinaryFilePatch createShelvedBinarySimplePatch(@NotNull String binaryFilename) { + return new ShelvedBinaryFilePatch(new ShelvedBinaryFile(binaryFilename, binaryFilename, null)); + } + + // inspired by IDEA-109608 + public void testFileAdditionGoesIntoCorrectFolder() throws Exception { + String path = "platform/util/src/com/intellij/util/io/SomeNewFile.java"; + TextFilePatch patch = createFileAddition(path); + checkSingleFileOperationAmongSimilarFolders(path, patch); + } + + public void testFileDeletionFromCorrectFolder() throws Exception { + String path = "platform/util/src/com/intellij/util/io/G.java"; + TextFilePatch patch = createFileDeletion(path); + checkSingleFileOperationAmongSimilarFolders(path, patch); + } + + public void testFileModificationFromCorrectFolder() throws Exception { + String path = "platform/platform-impl/src/io/B.java"; + TextFilePatch patch = create(path); + checkSingleFileOperationAmongSimilarFolders(path, patch); + } + + private void checkSingleFileOperationAmongSimilarFolders(final String filePath, final TextFilePatch patch){ + final VirtualFile root = myProject.getBaseDir(); + PsiTestUtil.addContentRoot(myModule, root); + VfsTestUtil.createFile(root, "platform/platform-impl/src/com/intellij/util/io/A.java"); + VfsTestUtil.createFile(root, "platform/platform-impl/src/io/B.java"); + VfsTestUtil.createFile(root, "platform/util-rt/src/com/intellij/util/io/C.java"); + VfsTestUtil.createFile(root, "platform/testFramework/src/com/intellij/util/io/D.java"); + VfsTestUtil.createFile(root, "platform/util/testSrc/com/intellij/openapi/util/io/E.java"); + VfsTestUtil.createFile(root, "platform/util/testSrc/com/intellij/util/io/F.java"); + VfsTestUtil.createFile(root, "platform/util/src/com/intellij/util/io/G.java"); + VfsTestUtil.createFile(root, "a/platform/util/src/com/intellij/util/io/H.java"); + VfsTestUtil.createFile(root, "platform/util/src/com/intellij/openapi/util/io/I.java"); + VfsTestUtil.createFile(root, "platform/util/completely/different/folder/J.java"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Collections.singletonList(patch)); + checkPath(result, filePath, Collections.singletonList(root), 0); + } + + // inspired by IDEA-118644 + public void testFileAdditionToNonexistentSubfolder() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + PsiTestUtil.addContentRoot(myModule, root); + VfsTestUtil.createDir(root, "platform/editor-ui-ex/src/com/intellij/openapi/editor/colors"); + VfsTestUtil.createDir(root, "plugins/properties/src/com/intellij/openapi/options/colors"); + VfsTestUtil.createDir(root, "platform/platform-api/src/com/intellij/openapi/editor/colors"); + VfsTestUtil.createDir(root, "java/java-impl/src/com/intellij/openapi/options/colors"); + VfsTestUtil.createDir(root, "platform/lang-impl/src/com/intellij/openapi/options/colors"); + VfsTestUtil.createDir(root, "platform/platform-tests/testSrc/com/intellij/openapi/editor"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + String path = "platform/platform-tests/testSrc/com/intellij/openapi/editor/colors/impl/A.java"; + TextFilePatch patch = create(path); + final List result = iterator.execute(Collections.singletonList(patch)); + checkPath(result, path, Collections.singletonList(root), 0); + } + + public void testFileAdditionGeneratedFromSuperRoot() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + PsiTestUtil.addContentRoot(myModule, root); + VfsTestUtil.createDir(root, "editor-ui-ex/src/com/intellij/openapi/editor/colors"); + VfsTestUtil.createDir(root, "platform-api/src/com/intellij/openapi/editor/colors"); + VfsTestUtil.createDir(root, "lang-impl/src/com/intellij/openapi/options/colors"); + VfsTestUtil.createDir(root, "platform-tests/testSrc/com/intellij/openapi/editor"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + String prefix = "community/platform/"; + String path = "platform-tests/testSrc/com/intellij/openapi/editor/colors/A.java"; + TextFilePatch patch = create(prefix + path); + final List result = iterator.execute(Collections.singletonList(patch)); + checkPath(result, path, Collections.singletonList(root), StringUtil.split(prefix, "/").size()); + } + + private static void checkPath(List filePatchInProgresses, String path, List bases, int strip) { + for (AbstractFilePatchInProgress patch : filePatchInProgresses) { + if (bases.contains(patch.getBase()) && path.equals(patch.getCurrentPath()) && (patch.getCurrentStrip() == strip)) { + return; + } + } + assertTrue("Failed for (first base only shown) '" + bases.iterator().next().getPath() + " + " + path + " " + strip + + "'; results: " + printPatches(filePatchInProgresses), false); + } + + private static String printPatches(final List filePatchInProgresses) { + final StringBuilder sb = new StringBuilder(); + for (AbstractFilePatchInProgress patch : filePatchInProgresses) { + sb.append("\n").append(patch.getBase().getPath()).append(" + ").append(patch.getCurrentPath()). + append(' ').append(patch.getCurrentStrip()); + } + return sb.toString(); + } + + // 2. files can be for 1 dir and 1 strip distance + public void testOneBaseAndStrip() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + createChildData(c, "f1.txt"); + createChildData(c, "f2.txt"); + createChildData(c, "f3.txt"); + createChildData(c, "f4.txt"); + createChildData(c, BINARY_FILENAME); + + TextFilePatch patch1 = create("t/b/c/f1.txt"); + TextFilePatch patch2 = create("t/b/c/f2.txt"); + TextFilePatch patch3 = create("t/b/c/f3.txt"); + TextFilePatch patch4 = create("t/b/c/f4.txt"); + ShelvedBinaryFilePatch shelvedBinaryPatch = createShelvedBinarySimplePatch("t/b/c/" + BINARY_FILENAME); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Arrays.asList(patch1, patch2, patch3, patch4, shelvedBinaryPatch)); + + checkPath(result, "b/c/f1.txt", Collections.singletonList(a), 1); + checkPath(result, "b/c/f2.txt", Collections.singletonList(a), 1); + checkPath(result, "b/c/f3.txt", Collections.singletonList(a), 1); + checkPath(result, "b/c/f4.txt", Collections.singletonList(a), 1); + checkPath(result, "b/c/" + BINARY_FILENAME, Collections.singletonList(a), 1); + } + + // 3. files can be with 2 base dirs and 1-one distance, 2-different distances + public void testOneBaseAndDifferentStrips() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + createChildData(c, "f1.txt"); + createChildData(c, "f2.txt"); + createChildData(c, "f3.txt"); + createChildData(c, "f4.txt"); + + VirtualFile e = createChildDirectory(root, "e"); + VirtualFile h1 = createChildDirectory(e, "h1"); + VirtualFile a1 = createChildDirectory(e, "a1"); + + VirtualFile f = createChildDirectory(a1, "b1"); + VirtualFile g = createChildDirectory(f, "c"); + createChildData(g, "f1.txt"); + createChildData(g, "f2.txt"); + createChildData(g, "f3.txt"); + createChildData(g, "f4.txt"); + + VirtualFile c2 = createChildDirectory(h1, "c"); + createChildData(c2, "f2.txt"); + + final TextFilePatch patch1 = create("a1/b1/c/f1.txt"); + final TextFilePatch patch2 = create("h1/c/f2.txt"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Arrays.asList(patch1, patch2)); + checkPath(result, "a1/b1/c/f1.txt", Collections.singletonList(e), 0); + checkPath(result, "h1/c/f2.txt", Collections.singletonList(e), 0); + } + + public void testPreviousFirstVariantAlsoMatches() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + createChildData(c, "f1.txt"); + createChildData(c, "f2.txt"); + createChildData(c, "f3.txt"); + createChildData(c, "f4.txt"); + createChildData(c, BINARY_FILENAME); + + final TextFilePatch patch1 = create("a1/b1/c/f1.txt"); + final TextFilePatch patch2 = create("h1/c/f2.txt"); + final ShelvedBinaryFilePatch shelvedBinaryPatch = createShelvedBinarySimplePatch("a1/b1/c/" + BINARY_FILENAME); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Arrays.asList(patch1, patch2, shelvedBinaryPatch)); + checkPath(result, "c/f1.txt", Collections.singletonList(b), 2); + checkPath(result, "c/f2.txt", Collections.singletonList(b), 1); + checkPath(result, "c/" + BINARY_FILENAME, Collections.singletonList(b), 2); + } + + public void testDefaultStrategyWorks() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile b = createChildDirectory(a, "b"); + VirtualFile c = createChildDirectory(b, "c"); + createChildData(c, "f1.txt"); + createChildData(c, "f2.txt"); + createChildData(c, "f3.txt"); + createChildData(c, "f4.txt"); + + TextFilePatch patch1 = create("a1/b1/c/f1.txt"); + TextFilePatch patch2 = create("h1/cccc/f2.txt"); + TextFilePatch patch3 = create("b/c/f3.txt"); + TextFilePatch patch4 = create("b/ccc/f4.txt"); + TextFilePatch patch5 = create("h1/c/f10.txt"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(Arrays.asList(patch1, patch2, patch3, patch4, patch5)); + checkPath(result, "c/f1.txt", Collections.singletonList(b), 2); + checkPath(result, "f2.txt", Collections.singletonList(c), 2); + checkPath(result, "b/c/f3.txt", Collections.singletonList(a), 0); + checkPath(result, "f4.txt", Collections.singletonList(c), 2); + checkPath(result, "h1/c/f10.txt", Collections.singletonList(root), 0); + } + + public void testExactWins() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile mod1 = createChildDirectory(a, "mod1"); + VirtualFile mod2 = createChildDirectory(a, "mod2"); + VirtualFile b1 = createChildDirectory(mod1, "b"); + VirtualFile b2 = createChildDirectory(mod2, "b"); + VirtualFile c1 = createChildDirectory(b1, "c"); + VirtualFile c2 = createChildDirectory(b2, "c"); + + createChildData(c1, "f1.txt"); + createChildData(c2, "f1.txt"); + createChildData(c2, "f10.txt"); + createChildData(c2, "f19.txt"); + createChildData(c2, "f18.txt"); + createChildData(c1, BINARY_FILENAME); + createChildData(c2, BINARY_FILENAME); + + TextFilePatch patch1 = create("mod1/b/c/f1.txt"); + TextFilePatch patch2 = create("mod2/b/c/f1.txt"); + TextFilePatch patch3 = create("mod26/b4/c3/f188.txt"); + + ShelvedBinaryFilePatch shelvedBinaryPatch1 = createShelvedBinarySimplePatch("mod1/b/c/" + BINARY_FILENAME); + ShelvedBinaryFilePatch shelvedBinaryPatch2 = createShelvedBinarySimplePatch("mod2/b/c/" + BINARY_FILENAME); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = + iterator.execute(Arrays.asList(patch1, patch2, patch3, shelvedBinaryPatch1, shelvedBinaryPatch2)); + checkPath(result, "mod1/b/c/f1.txt", Collections.singletonList(a), 0); + checkPath(result, "mod2/b/c/f1.txt", Collections.singletonList(a), 0); + checkPath(result, "mod26/b4/c3/f188.txt", Collections.singletonList(root), 0); + checkPath(result, "mod1/b/c/" + BINARY_FILENAME, Collections.singletonList(a), 0); + checkPath(result, "mod2/b/c/" + BINARY_FILENAME, Collections.singletonList(a), 0); + } + + public void testFindByContext() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + VirtualFile a = createChildDirectory(root, "a"); + VirtualFile mod1 = createChildDirectory(a, "mod1"); + VirtualFile mod2 = createChildDirectory(a, "mod2"); + VirtualFile b1 = createChildDirectory(mod1, "b"); + VirtualFile b2 = createChildDirectory(mod2, "b"); + VirtualFile c1 = createChildDirectory(b1, "coupleFiles"); + VirtualFile c2 = createChildDirectory(b2, "coupleFiles"); + + VirtualFile f11 = createChildData(c1, "file1.txt"); + VirtualFile f12 = createChildData(c2, "file1.txt"); + setFileText(f11, "Health care and education, in my view, are next up for fundamental software-based transformation.\n" + + "My venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\n" + + "We believe both of these industries, which historically have been highly resistant to entrepreneurial change,\n" + + "are primed for tipping by great new software-centric entrepreneurs.\n" + + "\n" + + "Even national defense is increasingly software-based.\n" + + "The modern combat soldier is embedded in a web of software that provides intelligence, communications,\n" + + "logistics and weapons guidance.\n" + + "Software-powered drones launch airstrikes without putting human pilots at risk.\n" + + "Intelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\n" + + "555"); + setFileText(f12, "Health care and education, in my view, are next up for fundamental software-based transformation.\n" + + "My venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\n" + + "We believe both of these industries, which historically have been highly resistant to entrepreneurial change,\n" + + "are primed for tipping by great new software-centric entrepreneurs.\n" + + "\n" + + "Even national defense is increasingly software-based.\n" + + "The modern combat soldier is embedded in a web of software that provides intelligence, communications,\n" + + "logistics and weapons guidance.\n" + + "Software-powered drones launch airstrikes without putting human pilots at risk.\n" + + "Intelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\n" + + "\n" + + "Companies in every industry need to assume that a software revolution is coming.\n" + + "This includes even industries that are software-based today.\n" + + "Great incumbent software companies like Oracle and Microsoft are increasingly threatened with irrelevance\n" + + "by new software offerings like Salesforce.com and Android (especially in a world where Google owns a major handset maker).\n" + + "\n" + + "In some industries, particularly those with a heavy real-world component such as oil and gas,\n" + + "the software revolution is primarily an opportunity for incumbents.\n" + + "But in many industries, new software ideas will result in the rise of new Silicon Valley-style start-ups\n" + + "that invade existing industries with impunity.\n" + + "Over the next 10 years, the battles between incumbents and software-powered insurgents will be epic.\n" + + "Joseph Schumpeter, the economist who coined the term \"creative destruction,\" would be proud."); + + final List patches = new PatchReader("Index: coupleFiles/file1.txt\n" + + "IDEA additional info:\n" + + "Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n" + + "<+>UTF-8\n" + + "Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP\n" + + "<+>Health care and education, in my view, are next up for fundamental software-based transformation.\\nMy venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\\nWe believe both of these industries, which historically have been highly resistant to entrepreneurial change,\\nare primed for tipping by great new software-centric entrepreneurs.\\n\\nEven national defense is increasingly software-based.\\nThe modern combat soldier is embedded in a web of software that provides intelligence, communications,\\nlogistics and weapons guidance.\\nSoftware-powered drones launch airstrikes without putting human pilots at risk.\\nIntelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\\n\\nCompanies in every industry need to assume that a software revolution is coming.\\nThis includes even industries that are software-based today.\\nGreat incumbent software companies like Oracle and Microsoft are increasingly threatened with irrelevance\\nby new software offerings like Salesforce.com and Android (especially in a world where Google owns a major handset maker).\\n\\nIn some industries, particularly those with a heavy real-world component such as oil and gas,\\nthe software revolution is primarily an opportunity for incumbents.\\nBut in many industries, new software ideas will result in the rise of new Silicon Valley-style start-ups\\nthat invade existing industries with impunity.\\nOver the next 10 years, the battles between incumbents and software-powered insurgents will be epic.\\nJoseph Schumpeter, the economist who coined the term \\\"creative destruction,\\\" would be proud.\n" + + "===================================================================\n" + + "--- coupleFiles/file1.txt\t(date 1351241865000)\n" + + "+++ coupleFiles/file1.txt\t(revision )\n" + + "@@ -15,7 +15,7 @@\n" + + " by new software offerings like Salesforce.com and Android (especially in a world where Google owns a major handset maker).\n" + + " \n" + + " In some industries, particularly those with a heavy real-world component such as oil and gas,\n" + + "-the software revolution is primarily an opportunity for incumbents.\n" + + "+the software revolution is primarily an opportunity for incumbents.Unique\n" + + " But in many industries, new software ideas will result in the rise of new Silicon Valley-style start-ups\n" + + " that invade existing industries with impunity.\n" + + " Over the next 10 years, the battles between incumbents and software-powered insurgents will be epic.\n" + + "\\ No newline at end of file\n").readTextPatches(); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(patches); + checkPath(result, "coupleFiles/file1.txt", Collections.singletonList(b2), 0); + } + + public void testFindByContext2() throws Exception { + final VirtualFile root = myProject.getBaseDir(); + + PsiTestUtil.addContentRoot(myModule, root); + + final VirtualFile a = createChildDirectory(root, "a"); + final VirtualFile mod1 = createChildDirectory(a, "mod1"); + final VirtualFile mod2 = createChildDirectory(a, "mod2"); + final VirtualFile b1 = createChildDirectory(mod1, "b"); + final VirtualFile b2 = createChildDirectory(mod2, "b"); + final VirtualFile c1 = createChildDirectory(b1, "coupleFiles"); + final VirtualFile c2 = createChildDirectory(b2, "coupleFiles"); + + final VirtualFile f11 = createChildData(c1, "file1.txt"); + final VirtualFile f12 = createChildData(c2, "file1.txt"); + setFileText(f11, "Health care and education, in my view, are next up for fundamental software-based transformation.\n" + + "My venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\n" + + "We believe both of these industries, which historically have been highly resistant to entrepreneurial change,\n" + + "are primed for tipping by great new software-centric entrepreneurs.\n" + + "\n" + + "Even national defense is increasingly software-based.\n" + + "The modern combat soldier is embedded in a web of software that provides intelligence, communications,\n" + + "logistics and weapons guidance.\n" + + "Software-powered drones launch airstrikes without putting human pilots at risk.\n" + + "Intelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\n" + + "555"); + setFileText(f12, "Health care and education, in my view, are next up for fundamental software-based transformation.\n" + + "My venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\n" + + "We believe both of these industries, which historically have been highly resistant to entrepreneurial change,\n" + + "are primed for tipping by great new software-centric entrepreneurs.\n" + + "\n" + + "Even national defense is increasingly software-based.\n" + + "The modern combat soldier is embedded in a web of software that provides intelligence, communications,\n" + + "logistics and weapons guidance.\n" + + "Software-powered drones launch airstrikes without putting human pilots at risk.\n" + + "Intelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\n" + + "\n" + + "Companies in every industry need to assume that a software revolution is coming.\n" + + "This includes even industries that are software-based today.\n" + + "Great incumbent software companies like Oracle and Microsoft are increasingly threatened with irrelevance\n" + + "by new software offerings like Salesforce.com and Android (especially in a world where Google owns a major handset maker).\n" + + "\n" + + "In some industries, particularly those with a heavy real-world component such as oil and gas,\n" + + "the software revolution is primarily an opportunity for incumbents.\n" + + "But in many industries, new software ideas will result in the rise of new Silicon Valley-style start-ups\n" + + "that invade existing industries with impunity.\n" + + "Over the next 10 years, the battles between incumbents and software-powered insurgents will be epic.\n" + + "Joseph Schumpeter, the economist who coined the term \"creative destruction,\" would be proud."); + + final List patches = new PatchReader("Index: coupleFiles/file1.txt\n" + + "IDEA additional info:\n" + + "Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n" + + "<+>UTF-8\n" + + "Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP\n" + + "<+>Health care and education, in my view, are next up for fundamental software-based transformation.\\nMy venture capital firm is backing aggressive start-ups in both of these gigantic and critical industries.\\nWe believe both of these industries, which historically have been highly resistant to entrepreneurial change,\\nare primed for tipping by great new software-centric entrepreneurs.\\n\\nEven national defense is increasingly software-based.\\nThe modern combat soldier is embedded in a web of software that provides intelligence, communications,\\nlogistics and weapons guidance.\\nSoftware-powered drones launch airstrikes without putting human pilots at risk.\\nIntelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\\n555\n" + + "===================================================================\n" + + "--- coupleFiles/file1.txt\t(date 1351242049000)\n" + + "+++ coupleFiles/file1.txt\t(revision )\n" + + "@@ -8,4 +8,4 @@\n" + + " logistics and weapons guidance.\n" + + " Software-powered drones launch airstrikes without putting human pilots at risk.\n" + + " Intelligence agencies do large-scale data mining with software to uncover and track potential terrorist plots.\n" + + "-555\n" + + "\\ No newline at end of file\n" + + "+555 ->\n" + + "\\ No newline at end of file\n").readTextPatches(); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List result = iterator.execute(patches); + checkPath(result, "coupleFiles/file1.txt", Collections.singletonList(b1), 0); + } + + public void testFindProjectDirBasedOrAccordingContext() throws Exception { + VirtualFile root = myProject.getBaseDir(); + PsiTestUtil.addContentRoot(myModule, root); + + createChildData(root, "fff1.txt"); + VirtualFile subdir = createChildDirectory(root, "subdir"); + VirtualFile wrongVariant = createChildData(subdir, "fff1.txt"); + + setFileText(wrongVariant, "aaaa\n" + + "bbbb\n" + + "dddd\n" + + "eeee"); + + List patches = new PatchReader("Index: fff1.txt\n" + + "IDEA additional info:\n" + + "Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\n" + + "<+>UTF-8\n" + + "===================================================================\n" + + "--- fff1.txt\t(date 1459006145000)\n" + + "+++ fff1.txt\t(revision )\n" + + "@@ -1,4 +1,5 @@\n" + + " aaaa\n" + + " bbbb\n" + + "+cccc\n" + + " dddd\n" + + " eeee\n" + + "\\ No newline at end of file\n").readTextPatches(); + + List result = new MatchPatchPaths(myProject).execute(patches, true); + checkPath(result, "fff1.txt", Collections.singletonList(root), 0); + result = new MatchPatchPaths(myProject).execute(patches); + checkPath(result, "fff1.txt", Collections.singletonList(subdir), 0); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchMatcherTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchMatcherTest.java new file mode 100644 index 000000000000..6b57916537b3 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/PatchMatcherTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.diff.impl.patch.TextFilePatch; +import com.intellij.openapi.vcs.changes.patch.AbstractFilePatchInProgress; +import com.intellij.openapi.vcs.changes.patch.MatchPatchPaths; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static com.intellij.openapi.vcs.PatchAutoInitTest.create; + + +public class PatchMatcherTest extends PlatformTestCase { + @Override + protected File getIprFile() throws IOException { + return new File(createTempDirectory(), "test.ipr"); + } + + public void testMatchPathAboveProject() { + final VirtualFile root = myProject.getBaseDir(); + VirtualFile vf = createChildData(root.getParent(), "file.txt"); + + final File ioFile = VfsUtilCore.virtualToIoFile(vf); + assertNotNull(ioFile); + myFilesToDelete.add(ioFile); + final TextFilePatch patch = create("../file.txt"); + + final MatchPatchPaths iterator = new MatchPatchPaths(myProject); + final List filePatchInProgresses = iterator.execute(Collections.singletonList(patch)); + + assertEquals(1, filePatchInProgresses.size()); + assertEquals(root.getParent(), filePatchInProgresses.get(0).getBase()); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RangeBuilderTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RangeBuilderTest.java new file mode 100644 index 000000000000..51ebca84a3a4 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RangeBuilderTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.vcs.ex.Range; +import com.intellij.openapi.vcs.ex.RangesBuilder; +import com.intellij.testFramework.LightPlatformTestCase; +import com.intellij.util.diff.FilesTooBigForDiffException; + +import java.util.Arrays; +import java.util.List; + +/** + * author: lesya + */ +public class RangeBuilderTest extends LightPlatformTestCase { + + public void testIdenticalContents() throws FilesTooBigForDiffException { + String upToDateContent = "a\na\na\na\n"; + assertTrue(RangesBuilder.createRanges(EditorFactory.getInstance().createDocument(upToDateContent), + EditorFactory.getInstance().createDocument(upToDateContent)).isEmpty()); + } + + public void testModified() throws FilesTooBigForDiffException { + doTest( + new String[]{"1", "2", "8", "3", "4"}, + new String[]{"1", "2", "9", "3", "4"}, + new Range[]{new Range(2, 3, 2, 3)} + ); + + + doTest( + new String[]{"1234", "2345", "3456"}, + new String[]{"1234", "23a45", "3456"}, + new Range[]{new Range(1, 2, 1, 2)} + ); + + doTest( + new String[]{"1234", "2345", "3456"}, + new String[]{"12a34", "2345", "3456"}, + new Range[]{new Range(0, 1, 0, 1)} + ); + + doTest( + new String[]{"abc"}, + new String[]{"anbnc"}, + new Range[]{new Range(0, 1, 0, 1)} + ); + + + + + } + + public void testDeleted() throws FilesTooBigForDiffException { + doTest(new String[]{"a", "a", "a", "b", "b", "b", "c", "c", "c"}, + new String[]{"a", "a", "a", "c", "c", "c"}, + new Range[]{new Range(3, 3, 3, 6)} + ); + + doTest( + new String[]{"1", "2", "3"}, + new String[]{"1", "2"}, + new Range[]{new Range(2, 2, 2, 3)} + ); + + doTest( + new String[]{"1", "2", "8", "3", "4"}, + new String[]{"1", "2", "3", "4"}, + new Range[]{new Range(2, 2, 2, 3)} + ); + + + } + + public void testInsert() throws FilesTooBigForDiffException { + doTest( + new String[]{"1", "3"}, + new String[]{"1", "2", "3"}, + new Range[]{new Range(1, 2, 1, 1)} + ); + + doTest( + new String[]{ "1", "3"}, + new String[]{"2", "1", "3"}, + new Range[]{new Range(0, 1, 0, 0)} + ); + doTest( + new String[]{"1", "2", "3", "4"}, + new String[]{"1", "2", "8", "3", "4"}, + new Range[]{new Range(2, 3, 2, 2)} + ); + + + + } + + + public void testInsertAtEnd() throws FilesTooBigForDiffException { + doTest("1", + "1\n", + new Range[]{new Range(1, 2, 1, 1)} + ); + + doTest( + new String[]{"1"}, + new String[]{"1", ""}, + new Range[]{new Range(2, 3, 2, 2)} + ); + + } + + + public void testDocument(){ + Document document = EditorFactory.getInstance().createDocument("1\n\n"); + int lineStartOffset = document.getLineStartOffset(document.getLineCount() - 1); + assertEquals(3, lineStartOffset); + document.getLineNumber(2); + } + + private static void doTest(String[] upToDate, String[] current, + Range[] expected) throws FilesTooBigForDiffException { + CharSequence upToDateContent = createContentOn(upToDate); + CharSequence currentContent = createContentOn(current); + doTest(upToDateContent, currentContent, expected); + } + + private static void doTest(CharSequence upToDateContent, CharSequence currentContent, Range[] expected) throws FilesTooBigForDiffException { + List result = RangesBuilder.createRanges(EditorFactory.getInstance().createDocument(currentContent), + EditorFactory.getInstance().createDocument(upToDateContent)); + assertEquals(Arrays.asList(expected), result); + } + + private static String createContentOn(String[] content) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < content.length; i++) { + result.append(content[i]); + result.append('\n'); + } + return result.toString(); + } + +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RollbackTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RollbackTest.java new file mode 100644 index 000000000000..8bdb255d52cd --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/RollbackTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2000-2017 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; + +import com.intellij.openapi.vcs.ex.Range; +import com.intellij.util.diff.FilesTooBigForDiffException; + +/** + * author: lesya + */ +public class RollbackTest extends BaseLineStatusTrackerTestCase{ + + public void testUpToDateContent1() throws FilesTooBigForDiffException { + createDocument("\n1\n2\n3\n4\n5\n6\n7"); + deleteString(0, 4); + assertEquals("1\n2", myTracker.getVcsContent(getFirstRange()).toString()); + } + + public void testUpToDateContent2() throws FilesTooBigForDiffException { + createDocument("\n1\n2\n3\n4\n5\n6\n7"); + deleteString(3, 5); + assertEquals("2", myTracker.getVcsContent(getFirstRange()).toString()); + } + + public void testRollbackInserted1() throws FilesTooBigForDiffException { + String initialContent = "1\n2\n3\n4"; + createDocument(initialContent); + insertString(7, "\n5\n6"); + compareRanges(); + rollbackFirstChange(Range.INSERTED); + assertEquals(initialContent, myDocument.getText()); + compareRanges(); + insertString(7, "\5\n6\n7\n"); + compareRanges(); + rollbackFirstChange(Range.MODIFIED); + compareRanges(); + } + + public void testRollbackInserted2() throws FilesTooBigForDiffException { + + doTestRollback("1\n2\n3\n4", () -> insertString(0, "\n0\n"), Range.INSERTED); + } + + public void testRollbackInserted3() throws FilesTooBigForDiffException { + doTestRollback("1\n2\n3\n4\n5\n6", () -> insertString(5, "\n0\n"), Range.INSERTED); + } + + public void testRollbackInserted4() throws FilesTooBigForDiffException { + doTestRollback("1\n2\n3\n4\n5", () -> insertString(myDocument.getTextLength(), "\n"), Range.INSERTED); + } + + public void testRollbackModified4() throws FilesTooBigForDiffException { + doTestRollback("1\n2\n3\n4", () -> insertString(0, "\n0\n0"), Range.MODIFIED); + + } + + public void testRollbackModified1() throws FilesTooBigForDiffException { + doTestRollback("1\n2\n3\n4\n5\n6\n7", () -> deleteString(0, 3), Range.MODIFIED); + } + + public void testRollbackDeleted2() throws FilesTooBigForDiffException { + doTestRollback("1\n2\n3\n4\n5\n6\n7", () -> deleteString(0, 4), Range.DELETED); + } + + public void testRollbackDeleted3() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(0, 3), Range.DELETED); + } + + public void testRollbackDeleted4() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(0, 4), Range.DELETED); + } + + public void testRollbackModified5() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(2, 5), Range.MODIFIED); + } + + public void testRollbackDeleted6() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(3, 5), Range.DELETED); + } + + public void testRollbackModified7() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(3, 6), Range.MODIFIED); + } + + public void testRollbackDeleted8() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(3, 7), Range.DELETED); + } + + public void testRollbackDeleted9() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(5, 13), Range.DELETED); + } + + public void testRollbackEmptyLastLineDeletion() throws FilesTooBigForDiffException { + String text1 = "1\n2\n3\n\n"; + String text2 = "1\n2\n3\n"; + createDocument(text2, text1); + rollback(myTracker.getRanges().get(0)); + + assertEquals(myDocument.getText(), text1); + assertEmpty(myTracker.getRanges()); + } + + public void testSRC27943() throws FilesTooBigForDiffException { + String initialContent = "<%@ taglib uri=\"/WEB-INF/sigpath.tld\" prefix=\"sigpath\" %>\n" + + "<%@ taglib uri=\"/WEB-INF/struts-html.tld\" prefix=\"html\" %>\n" + + "<%@ taglib uri=\"/WEB-INF/struts-bean.tld\" prefix=\"bean\" %>\n" + + "<%@ taglib uri=\"/WEB-INF/string.tld\" prefix=\"str\" %>\n" + + "<%@ taglib uri=\"/WEB-INF/regexp.tld\" prefix=\"rx\" %>"; + + String newContent = "<%@ taglib uri=\"/tag_lib/sigpath.tld\" prefix=\"sigpath\" %>\n" + + "<%@ taglib uri=\"/tag_lib/struts-html.tld\" prefix=\"html\" %>\n" + + "<%@ taglib uri=\"/tag_lib/struts-bean.tld\" prefix=\"bean\" %>\n" + + "<%@ taglib uri=\"/tag_lib/string.tld\" prefix=\"str\" %>\n" + + "<%@ taglib uri=\"/tag_lib/regexp.tld\" prefix=\"rx\" %>"; + + createDocument(initialContent); + replaceString(0, initialContent.length(), newContent); + compareRanges(); + rollbackFirstChange(Range.MODIFIED); + assertEquals(initialContent, myDocument.getText()); + compareRanges(); + } + + public void testRollbackModified10() throws FilesTooBigForDiffException { + doTestRollback("\n1\n2\n3\n4\n5\n6\n7", () -> deleteString(6, 13), Range.MODIFIED); + } + + public void testEmptyDocumentBug() throws Throwable { + createDocument(""); + + insertString(0, "adsf"); + rollbackFirstChange(Range.MODIFIED); + compareRanges(); + } + + private void doTestRollback(String initialContent, Runnable modifyAction, byte expectedRangeType) throws FilesTooBigForDiffException { + createDocument(initialContent); + modifyAction.run(); + compareRanges(); + rollbackFirstChange(expectedRangeType); + assertEquals(initialContent, myDocument.getText()); + compareRanges(); + } + + private void rollbackFirstChange(byte expectedRangeType) { + final Range range = getFirstRange(); + assertEquals(expectedRangeType, range.getType()); + rollback(range); + } + + private Range getFirstRange() { + assertEquals(1, myTracker.getRanges().size()); + return myTracker.getRanges().get(0); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BackgroundTaskQueueTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BackgroundTaskQueueTest.java new file mode 100644 index 000000000000..a2fbeae9d4d4 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BackgroundTaskQueueTest.java @@ -0,0 +1,389 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.progress.BackgroundTaskQueue; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.testFramework.EdtTestUtil; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.TimeoutUtil; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntConsumer; + +/** + *

Test for {@link BackgroundTaskQueue}.

+ *

As BackgroundTaskQueue has different execution strategy for tests and for production, this test case pretends not to be a test, + * but not in all cases. This is a bit hacky, but I didn't want to change {@link ProgressManagerImpl} logic. + *

    + *
  • Application IS in a unit test mode.
  • + *
  • Application IS in the headless mode, because it's needed to avoid showing UI.
  • + *
  • The executed {@link Task Tasks} are not headless.
  • + *
  • The test is started not from UI thread.
  • + *

+ */ +public class BackgroundTaskQueueTest extends PlatformTestCase { + private BackgroundTaskQueue myQueue; + private ThreadRunner myThreadRunner; + private Random myRandom; + + @Override + protected void setUp() throws Exception { + EdtTestUtil.runInEdtAndWait(() -> { + try { + super.setUp(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + myQueue = new BackgroundTaskQueue(myProject, "test queue"); + myQueue.setForceAsyncInTests(true, null); + }); + myThreadRunner = new ThreadRunner(); + myRandom = new Random(); + } + + @Override + protected void tearDown() throws Exception { + myThreadRunner.finish(); + + EdtTestUtil.runInEdtAndWait((() -> { + myQueue.clear(); + myQueue = null; + + try { + super.tearDown(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + @Override + protected boolean runInDispatchThread() { + return false; + } + + public void testSingleSuccessfullTask() throws InterruptedException { + TestTask task = new TestTask(); + myQueue.run(task); + waitForTasks(task); + assertSucceeded(task); + } + + public void testSingleCancelledTask() throws InterruptedException { + TestTask task = new TestTask() { + @Override + protected void execute(ProgressIndicator indicator) { + sleep50(); + indicator.cancel(); + } + }; + myQueue.run(task); + waitForTasks(task); + assertEquals(TaskState.CANCELLED, task.getState()); + } + + public void testSingleExceptionTask() throws InterruptedException { + TestTask task = new TestTask() { + @Override + protected void execute(ProgressIndicator indicator) { + sleep50(); + throw new NullPointerException("NPE"); + } + }; + myQueue.run(task); + waitForTasks(task); + assertEquals(TaskState.EXCEPTION, task.getState()); + } + + /** + * Start one task several times from several threads. + * Finally task should complete successfully. + */ + public void testOneTaskRunSeveralTimes() throws InterruptedException { + final int THREADS = 3; + final int RUNS_PER_THREAD = 10; + final int RUNS = THREADS * RUNS_PER_THREAD; + + Semaphore semaphore = new Semaphore(1 - RUNS); + + int[] succeeded = new int[]{0}; + final Task.Backgroundable task = new Task.Backgroundable(getProject(), "Test Task", true) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + succeeded[0]++; + semaphore.release(); + } + }; + + myThreadRunner.run(THREADS, (i) -> { + for (int j = 0; j < RUNS_PER_THREAD; j++) { + myQueue.run(task); + } + }); + + semaphore.tryAcquire(RUNS, 1000, TimeUnit.MILLISECONDS); + assertEquals(RUNS, succeeded[0]); + } + + /** + * Start several tasks from a single thread. Wait for all to successfully complete. + */ + public void testSeveralTasksStartedFromSingleThread() throws InterruptedException { + TestTask[] tasks = createSeveralTasks(); + for (TestTask task : tasks) { + myQueue.run(task); + } + waitForTasks(tasks); + assertTaskState(tasks, TaskState.SUCCEEDED); + } + + /** + * Start several tasks from different threads. All should successfully complete. + */ + public void testSeveralSuccessfulTasksStartedFromDifferentThreads() throws InterruptedException { + final TestTask[] tasks = createSeveralTasks(); + + myThreadRunner.run(tasks.length, (i) -> myQueue.run(tasks[i])); + + waitForTasks(tasks); + assertTaskState(tasks, TaskState.SUCCEEDED); + } + + /** + * Create 18 tasks: 6 successful, 6 cancelled, 6 throwing exception. Start them from different threads, so that each thread run + * tasks with different result. + */ + public void testSeveralDifferentlyEndingTasksStartedFromDifferentThreads() throws InterruptedException { + final TestTask[] successful = new TestTask[6]; + final TestTask[] cancelled = new TestTask[6]; + final TestTask[] exceptioned = new TestTask[6]; + for (int i = 0; i < 6; i++) { + successful[i] = new TestTask(); + } + for (int i = 0; i < 6; i++) { + cancelled[i] = new TestTask() { + @Override + protected void execute(ProgressIndicator indicator) { + sleep50(); + throw new ProcessCanceledException(); + } + }; + } + for (int i = 0; i < 6; i++) { + exceptioned[i] = new TestTask() { + @Override + protected void execute(ProgressIndicator indicator) { + sleep50(); + throw new RuntimeException(); + } + }; + } + + myThreadRunner.run(3, (i) -> { + myQueue.run(successful[i]); + myQueue.run(successful[i + 3]); + myQueue.run(cancelled[i]); + myQueue.run(cancelled[i + 3]); + myQueue.run(exceptioned[i]); + myQueue.run(exceptioned[i + 3]); + }); + + waitForTasks(successful); + waitForTasks(cancelled); + waitForTasks(exceptioned); + + assertTaskState(successful, TaskState.SUCCEEDED); + assertTaskState(cancelled, TaskState.CANCELLED); + assertTaskState(exceptioned, TaskState.EXCEPTION); + } + + public void testTasksAreNotParallel() throws Exception { + final int THREADS = 3; + final int RUNS_PER_THREAD = 10; + final int RUNS = THREADS * RUNS_PER_THREAD; + + final boolean[] bool = new boolean[]{false}; + final Semaphore semaphore = new Semaphore(1 - RUNS); + + final Task.Backgroundable task = new Task.Backgroundable(myProject, "Test", false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + Assert.assertFalse(bool[0]); + bool[0] = true; + sleepX(17); + semaphore.release(); + Assert.assertTrue(bool[0]); + bool[0] = false; + } + }; + + final AtomicInteger cntThreads = new AtomicInteger(THREADS); + myThreadRunner.run(THREADS, (i) -> { + for (int j = 0; j < RUNS_PER_THREAD; j++) { + sleepX(7); + myQueue.run(task); + } + cntThreads.decrementAndGet(); + }); + + semaphore.tryAcquire(RUNS, 5000, TimeUnit.MILLISECONDS); + + Assert.assertTrue(myQueue.isEmpty()); + Assert.assertEquals(0, cntThreads.get()); + myThreadRunner.finish(); + } + + + private void assertSucceeded(TestTask task) { + assertEquals(TaskState.SUCCEEDED, task.getState()); + } + + private void assertTaskState(TestTask[] tasks, TaskState state) { + for (TestTask task : tasks) { + assertEquals(state, task.getState()); + } + } + + private TestTask[] createSeveralTasks() { + final TestTask[] tasks = new TestTask[10]; + for (int i = 0; i < tasks.length; i++) { + tasks[i] = new TestTask(); + } + return tasks; + } + + private void waitForTasks(TestTask... tasks) throws InterruptedException { + for (TestTask task : tasks) { + task.waitFor(1000); + } + } + + private static void sleep50() { + TimeoutUtil.sleep(50); + } + + private void sleepX(final int intervalMs) { + TimeoutUtil.sleep(myRandom.nextInt(intervalMs) + 1); + } + + private enum TaskState { + CREATED, RUNNING, SUCCEEDED, EXCEPTION, CANCELLED; + + boolean isComplete() { + return this == SUCCEEDED || this == EXCEPTION || this == CANCELLED; + } + } + + private class TestTask extends Task.Backgroundable { + private final AtomicReference myState = new AtomicReference<>(TaskState.CREATED); + private final Semaphore mySemaphore = new Semaphore(0); + + public TestTask() { + super(BackgroundTaskQueueTest.this.getProject(), "Test Task", true); + } + + protected void execute(ProgressIndicator indicator) { + for (int i = 0; i < 10000; i++) { + Math.sin(i); + } + } + + @NotNull + public TaskState getState() { + return myState.get(); + } + + public boolean isComplete() { + return myState.get().isComplete(); + } + + + @Override + public final void run(@NotNull ProgressIndicator indicator) { + myState.compareAndSet(TaskState.CREATED, TaskState.RUNNING); + execute(indicator); + } + + @Override + public final void onCancel() { + myState.compareAndSet(TaskState.RUNNING, TaskState.CANCELLED); + } + + @Override + public final void onSuccess() { + myState.compareAndSet(TaskState.RUNNING, TaskState.SUCCEEDED); + } + + @Override + public final void onThrowable(@NotNull Throwable error) { + myState.compareAndSet(TaskState.RUNNING, TaskState.EXCEPTION); + } + + @Override + public final void onFinished() { + mySemaphore.release(); + assertTrue(myState.get() != TaskState.RUNNING); + assertTrue(myState.get() != TaskState.CREATED); + } + + public void waitFor(int timeout) throws InterruptedException { + assertTrue(mySemaphore.tryAcquire(1, timeout, TimeUnit.MILLISECONDS)); + mySemaphore.release(); + } + + @Override + public boolean isHeadless() { + return false; + } + } + + private static class ThreadRunner { + private final List myThreads = new ArrayList<>(); + + public void run(int count, IntConsumer task) { + for (int i = 0; i < count; i++) { + int threadIndex = i; + Thread thread = new Thread("BTQ-" + threadIndex) { + @Override + public void run() { + task.accept(threadIndex); + } + }; + thread.start(); + myThreads.add(thread); + } + } + + public void finish() { + ConcurrencyUtil.joinAll(myThreads); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BufferedListConsumerTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BufferedListConsumerTest.java new file mode 100644 index 000000000000..66dee9f8f736 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/BufferedListConsumerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.testFramework.vcs.FileBasedTest; +import com.intellij.util.BufferedListConsumer; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.Function; +import com.intellij.util.concurrency.Semaphore; +import junit.framework.Assert; +import org.junit.Test; + +import java.util.*; + +/** + * Created with IntelliJ IDEA. + * User: Irina.Chernushina + * Date: 1/17/13 + * Time: 4:35 PM + */ +public class BufferedListConsumerTest extends FileBasedTest { + + @Test + public void testHugeWriteRead() throws Exception { + List threads = new ArrayList<>(); + final Random random = new Random(17); + final Set src = new HashSet<>(200); + for (int i = 0; i < 100; i++) { + src.add(System.currentTimeMillis()); + src.add(random.nextLong()); + } + final List dst = new ArrayList<>(); + final BufferedListConsumer consumer = new BufferedListConsumer<>(9, items -> dst.addAll(items), 4); + final Semaphore semaphore = new Semaphore(); + semaphore.down(); + Thread thread1 = new Thread("buffered list test") { + @Override + public void run() { + for (Long aLong : src) { + consumer.consumeOne(aLong); + } + consumer.flush(); + semaphore.up(); + } + }; + thread1.start(); + threads.add(thread1); + + final long timeout = 10 * 1000; + final long start = System.currentTimeMillis(); + while ((System.currentTimeMillis() - start) < timeout) { + semaphore.waitFor(50); + if (dst.size() == src.size()) break; + } + + boolean equal = src.size() == dst.size(); + if (equal) { + for (int i = 0; i < dst.size(); i++) { + Long dstL = dst.get(i); + if (! src.contains(dstL)) { + System.out.println("i = " + i); + equal = false; + break; + } + } + } + if (! equal) { + System.out.println("src: " + src.size() + ", dst: " + dst.size()); + final Function f = aLong -> String.valueOf(aLong); + System.out.println("Contents: src: [" + StringUtil.join(src, f, ", ") + "}\n\n\ndst: [" + + StringUtil.join(dst, f, ", ") + "]\n"); + } + Assert.assertTrue(equal); + ConcurrencyUtil.joinAll(threads); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedChangesCacheTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedChangesCacheTest.java new file mode 100644 index 000000000000..6c185530dcab --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedChangesCacheTest.java @@ -0,0 +1,398 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.EmptyRunnable; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.RepositoryLocation; +import com.intellij.openapi.vcs.VcsDirectoryMapping; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vcs.update.FileGroup; +import com.intellij.openapi.vcs.update.UpdatedFiles; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PsiTestUtil; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.vcsUtil.VcsUtil; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author yole + */ +public class CommittedChangesCacheTest extends PlatformTestCase { + private MockAbstractVcs myVcs; + private MockCommittedChangesProvider myProvider; + private MockDiffProvider myDiffProvider; + private CommittedChangesCache myCache; + private ProjectLevelVcsManagerImpl myVcsManager; + private File myTempDir; + private VirtualFile myContentRoot; + private MockListener myListener; + private MessageBusConnection myConnection; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + myVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(getProject()); + myVcsManager.waitForInitialized(); + + myVcs = new MockAbstractVcs(getProject()); + myProvider = new MockCommittedChangesProvider(); + myVcs.setCommittedChangesProvider(myProvider); + myDiffProvider = new MockDiffProvider(); + myVcs.setDiffProvider(myDiffProvider); + + myVcsManager.registerVcs(myVcs); + myVcsManager.setDirectoryMappings(Arrays.asList(new VcsDirectoryMapping("", myVcs.getName()))); + + myCache = CommittedChangesCache.getInstance(getProject()); + + myTempDir = createTempDirectory(); + myContentRoot = getVirtualFile(myTempDir); + PsiTestUtil.addContentRoot(myModule, myContentRoot); + myFilesToDelete.add(myCache.getCachesHolder().getCacheBasePath()); + } + + @Override + protected void tearDown() throws Exception { + if (myConnection != null) { + myConnection.disconnect(); + myConnection = null; + } + myVcsManager.unregisterVcs(myVcs); + myVcsManager = null; + myVcs = null; + myProvider = null; + myDiffProvider = null; + myCache.clearCaches(EmptyRunnable.INSTANCE); + myCache = null; + myContentRoot = null; + myListener = null; + super.tearDown(); + } + + public void testEmpty() throws Exception { + final List list = myCache.getChanges(myProvider.createDefaultSettings(), myContentRoot, myVcs, 0, false, + myProvider, myProvider.getLocationFor(VcsUtil.getFilePath(myContentRoot))); + assertEquals(0, list.size()); + } + + public void testSimple() throws Exception { + myProvider.registerChangeList("test"); + final List list = myCache.getChanges(myProvider.createDefaultSettings(), myContentRoot, myVcs, 0, false, + myProvider, myProvider.getLocationFor(VcsUtil.getFilePath(myContentRoot))); + assertEquals(1, list.size()); + assertEquals("test", list.get(0).getName()); + } + + public void testIncomingChangesSimple() throws Exception { + myProvider.registerChangeList("test"); + myCache.refreshAllCaches(); + final List list = getIncomingChangesFromCache(); + assertEquals(1, list.size()); + } + + private List getIncomingChangesFromCache() { + final List result = new ArrayList<>(); + // this is actually synchronous in tests + myCache.loadIncomingChangesAsync(committedChangeLists -> result.addAll(committedChangeLists), true); + return result; + } + + public void testUpdatedFilesSimple() throws Exception { + final Change change = createChange("1.txt", 2); + myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + attachListener(); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(0, getIncomingChangesFromCache().size()); + assertEquals(2, myListener.getIncomingChangesUpdateCount()); + final List list = myListener.getIncomingChangesUpdate(0); + assertEquals(1, list.size()); + } + + public void testFileUpdatedTwice() throws Exception { + final Change change = createChange("1.txt", 2); + final Change change2 = createChange("1.txt", 3); + myProvider.registerChangeList("test", change); + myProvider.registerChangeList("test 2", change2); + myCache.refreshAllCaches(); + int count = myProvider.getRefreshCount(); + assertEquals(2, getIncomingChangesFromCache().size()); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(1, getIncomingChangesFromCache().size()); + assertEquals(count, myProvider.getRefreshCount()); + } + + public void testFileUpdatedTwiceInOneStep() throws Exception { + final Change change = createChange("1.txt", 2); + final Change change2 = createChange("1.txt", 3); + myProvider.registerChangeList("test", change); + myProvider.registerChangeList("test 2", change2); + myCache.refreshAllCaches(); + assertEquals(2, getIncomingChangesFromCache().size()); + myCache.processUpdatedFiles(createUpdatedFiles(change2)); + assertEquals(0, getIncomingChangesFromCache().size()); + } + + public void testIncomingNotLast() throws Exception { + final Change change = createChange("1.txt", 2); + final Change change2 = createChange("2.txt", 2); + myProvider.registerChangeList("test", change); + myProvider.registerChangeList("test 2", change2); + myCache.refreshAllCaches(); + assertEquals(2, getIncomingChangesFromCache().size()); + myCache.processUpdatedFiles(createUpdatedFiles(change2)); + assertEquals(1, getIncomingChangesFromCache().size()); + } + + public void testRefreshRequired() throws Exception { + myCache.refreshAllCaches(); + final Change change = createChange("1.txt", 2); + myProvider.registerChangeList("test", change); + int count = myProvider.getRefreshCount(); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(0, getIncomingChangesFromCache().size()); + assertEquals(count+1, myProvider.getRefreshCount()); + } + + public void testCachedDate() throws Exception { + final Change change = createChange("1.txt", 2); + final CommittedChangeList list = myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + VirtualFile baseDir = myProject.getBaseDir(); + final ChangesCacheFile cacheFile = myCache.getCachesHolder().getCacheFile(myVcs, baseDir, myProvider.getLocationFor(VcsUtil.getFilePath(baseDir))); + assertEquals(list.getCommitDate(), cacheFile.getLastCachedDate()); + assertEquals(list.getCommitDate(), cacheFile.getFirstCachedDate()); + assertEquals(list.getNumber(), cacheFile.getLastCachedChangelist()); + assertTrue(cacheFile.hasCompleteHistory()); + } + + public void testPartialUpdate() throws Exception { + final Change change = createChange("1.txt", 2); + final Change change2 = createChange("2.txt", 2); + myProvider.registerChangeList("test", change, change2); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(1, getIncomingChangesFromCache().size()); + myCache.processUpdatedFiles(createUpdatedFiles(change2)); + assertEquals(0, getIncomingChangesFromCache().size()); + } + + public void testRefreshIncoming() throws Exception { + final String fileName = "1.txt"; + final File testFile = createTestFile(fileName); + final Change change = createChange(fileName, 2); + myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + myDiffProvider.setCurrentRevision(getVirtualFile(testFile), new VcsRevisionNumber.Int(2)); + boolean result = myCache.refreshIncomingChanges(); + assertTrue(result); + assertEquals(0, getIncomingChangesFromCache().size()); + result = myCache.refreshIncomingChanges(); + assertFalse(result); + } + + public void testDelete() throws Exception { + final Change change = MockCommittedChangesProvider.createMockDeleteChange(new File(myTempDir, "1.txt").toString(), 1); + myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + attachListener(); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(0, getIncomingChangesFromCache().size()); + assertEquals(2, myListener.getIncomingChangesUpdateCount()); + final List list = myListener.getIncomingChangesUpdate(0); + assertEquals(1, list.size()); + } + + public void testRefreshIncomingDeleted() throws Exception { + final Change change = createChange("1.txt", 2); + final Change change2 = MockCommittedChangesProvider.createMockDeleteChange(new File(myTempDir, "1.txt").toString(), 2); + myProvider.registerChangeList("test", change); + myProvider.registerChangeList("test 2", change2); + myCache.refreshAllCaches(); + assertEquals(2, getIncomingChangesFromCache().size()); + myCache.refreshIncomingChanges(); + assertEquals(0, getIncomingChangesFromCache().size()); + } + + public void testRefreshIncomingCDC() throws Exception { + final String fileName = "1.txt"; + final File testFile = createTestFile(fileName); + final Change change = createChange(fileName, 2); + final Change change2 = MockCommittedChangesProvider.createMockDeleteChange(testFile.toString(), 2); + final Change change3 = MockCommittedChangesProvider.createMockCreateChange(testFile.toString(), 3); + myProvider.registerChangeList("test", change); + myProvider.registerChangeList("test 2", change2); + myProvider.registerChangeList("test 3", change3); + myCache.refreshAllCaches(); + assertEquals(3, getIncomingChangesFromCache().size()); + myDiffProvider.setCurrentRevision(getVirtualFile(testFile), new VcsRevisionNumber.Int(3)); + boolean result = myCache.refreshIncomingChanges(); + assertTrue(result); + assertEquals(0, getIncomingChangesFromCache().size()); + } + + public void testUpdatedFilesNotify() throws Exception { + final Change change = createChange("1.txt", 2); + myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + + attachListener(); + myCache.processUpdatedFiles(createUpdatedFiles(change)); + assertEquals(1, myListener.getIncomingChangesUpdateCount()); + } + + public void testGetIncomingChangelist() throws Exception { + final String fileName = "1.txt"; + final File testFile = createTestFile(fileName); + final Change change = createChange(fileName, 2); + final CommittedChangeList list = myProvider.registerChangeList("test", change); + myCache.refreshAllCaches(); + assertEquals(1, getIncomingChangesFromCache().size()); + myCache.refreshIncomingChanges(); + myCache.loadIncomingChangesAsync(null, true); + final Pair incomingList = myCache.getIncomingChangeList(getVirtualFile(testFile)); + assertNotNull(incomingList); + assertEquals(list.getName(), incomingList.first.getName()); + } + + public void testGetIncomingChangelistPartial() throws Exception { + final String fileName = "1.txt"; + final String fileName2 = "2.txt"; + final File testFile = createTestFile(fileName); + final File testFile2 = createTestFile(fileName2); + final Change change = createChange(fileName, 2); + final Change change2 = createChange(fileName2, 2); + final CommittedChangeList list = myProvider.registerChangeList("test", change, change2); + myCache.refreshAllCaches(); + final VirtualFile vFile = getVirtualFile(testFile); + final VirtualFile vFile2 = getVirtualFile(testFile2); + myDiffProvider.setCurrentRevision(vFile, new VcsRevisionNumber.Int(2)); + myDiffProvider.setCurrentRevision(vFile2, new VcsRevisionNumber.Int(1)); + myCache.refreshIncomingChanges(); + myCache.loadIncomingChanges(false); + assertNull(myCache.getIncomingChangeList(vFile)); + final Pair incomingList = myCache.getIncomingChangeList(vFile2); + assertNotNull(incomingList); + assertEquals(list.getName(), incomingList.first.getName()); + } + + public void testInitCacheNotify() throws Exception { + final Change change = createChange("1.txt", 2); + myProvider.registerChangeList("test", change); + attachListener(); + myCache.refreshAllCaches(); + assertEquals(1, myListener.getLoadedChanges().size()); + } + + private void attachListener() { + myConnection = myCache.getMessageBus().connect(); + myListener = new MockListener(); + myConnection.subscribe(CommittedChangesCache.COMMITTED_TOPIC, myListener); + } + + private File createTestFile(final String fileName) throws IOException { + final File testFile = new File(myTempDir, fileName); + testFile.createNewFile(); + myFilesToDelete.add(testFile); + ApplicationManager.getApplication().runWriteAction(() -> { + VirtualFileManager.getInstance().syncRefresh(); + }); + return testFile; + } + + private UpdatedFiles createUpdatedFiles(final Change... changes) { + UpdatedFiles files = UpdatedFiles.create(); + for(Change change: changes) { + final ContentRevision afterRevision = change.getAfterRevision(); + if (afterRevision != null) { + files.getGroupById(FileGroup.MODIFIED_ID).add(afterRevision.getFile().getIOFile().getPath(), + myVcs.getKeyInstanceMethod(), afterRevision.getRevisionNumber()); + } + else { + final ContentRevision beforeRevision = change.getBeforeRevision(); + assert beforeRevision != null; + files.getGroupById(FileGroup.REMOVED_FROM_REPOSITORY_ID).add(beforeRevision.getFile().getIOFile().getPath(), + myVcs.getKeyInstanceMethod(), beforeRevision.getRevisionNumber()); + } + } + return files; + } + + private Change createChange(final String path, final int revision) { + return MockCommittedChangesProvider.createMockChange(new File(myTempDir, path).toString(), revision); + } + + private static class MockListener implements CommittedChangesListener { + private final List myLoadedChanges = new ArrayList<>(); + private final List> myIncomingChangesUpdates = new ArrayList<>(); + + @Override + public void changesLoaded(RepositoryLocation location, List changes) { + myLoadedChanges.addAll(changes); + } + + @Override + public void changesCleared() { + } + + @Override + public void presentationChanged() { + } + + @Override + public void incomingChangesUpdated(final List receivedChanges) { + myIncomingChangesUpdates.add(receivedChanges); + } + + @Override + public void refreshErrorStatusChanged(@Nullable VcsException lastError) { + } + + public int getIncomingChangesUpdateCount() { + return myIncomingChangesUpdates.size(); + } + + public List getLoadedChanges() { + return myLoadedChanges; + } + + public List getIncomingChangesUpdate(int index) { + return myIncomingChangesUpdates.get(index); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedListsSequencesZipperTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedListsSequencesZipperTest.java new file mode 100644 index 000000000000..6b80a1a3db6d --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/CommittedListsSequencesZipperTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.DefaultRepositoryLocation; +import com.intellij.openapi.vcs.RepositoryLocation; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeListImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.assertTrue; + +public class CommittedListsSequencesZipperTest { + + @Test + public void testSimple() throws Exception { + long[][] valuesWithCount = {{1, 3}, {2, 2}, {3, 1}, {5, 1}, {7, 2}, {8, 1}, {17, 1}, {18, 1}, {21, 2}}; + + check(valuesWithCount, list(1, 2, 7, 8, 18, 21), list(1, 2, 5, 7, 21), list(1, 3, 17)); + } + + @Test + public void testVar1() throws Exception { + long[][] valuesWithCount = {{1, 1}, {2, 1}, {3, 1}, {5, 1}, {6, 1}, {7, 1}, {11, 1}, {17, 1}, {18, 1}, {22, 1}, {111, 1}}; + + check(valuesWithCount, list(1, 7, 11, 111), list(2, 6, 18, 22), list(3, 5, 17)); + } + + @Test + public void testVar2() throws Exception { + long[][] valuesWithCount = {{1, 1}, {2, 1}, {3, 1}, {5, 1}, {6, 1}, {7, 2}, {11, 1}, {17, 1}, {18, 1}, {22, 1}, {111, 1}}; + + check(valuesWithCount, list(1, 7, 11, 111), list(2, 6, 7, 18, 22), list(3, 5, 17)); + } + + @Test + public void testVar3() throws Exception { + long[][] valuesWithCount = {{1, 2}, {2, 1}, {3, 1}, {5, 1}, {6, 1}, {7, 1}, {11, 1}, {17, 1}, {18, 1}, {22, 1}, {111, 1}}; + + check(valuesWithCount, list(1, 7, 11, 111), list(1, 2, 6, 18, 22), list(3, 5, 17)); + } + + @Test + public void testVar4() throws Exception { + long[][] valuesWithCount = {{1, 1}, {2, 1}, {3, 1}, {5, 1}, {6, 1}, {7, 1}, {11, 1}, {17, 1}, {18, 1}, {22, 1}, {111, 3}}; + + check(valuesWithCount, list(1, 7, 11, 111), list(2, 6, 18, 22, 111), list(3, 5, 17, 111)); + } + + @Test + public void testSame() throws Exception { + long[][] valuesWithCount = {{1, 3}, {7, 3}, {11, 3}, {111, 3}}; + + check(valuesWithCount, list(1, 7, 11, 111), list(1, 7, 11, 111), list(1, 7, 11, 111)); + } + + private static void check(@NotNull long[][] expected, @NotNull List... lists) { + CommittedListsSequencesZipper zipper = new CommittedListsSequencesZipper(Convertor.ourInstance); + int id = 0; + + for (List list : lists) { + zipper.add(new DefaultRepositoryLocation(String.valueOf(id++)), list); + } + + checkResult(zipper.execute(), expected); + } + + private static void checkResult(@NotNull List result, @NotNull long[]... numbers) { + final Set nums = new HashSet<>(); + final Map zipped = new HashMap<>(); + for (long[] pair : numbers) { + assertTrue(pair.length == 2); + nums.add(pair[0]); + if (pair[1] != 1) { + zipped.put(pair[0], (int) pair[1]); + } + } + + long previous = -1; + for (CommittedChangeList list : result) { + assertTrue("Ordering error: " + list.getNumber(), previous <= list.getNumber()); + assertTrue("Result does not contain: " + list.getNumber(), nums.contains(list.getNumber())); + + final Integer num = zipped.get(list.getNumber()); + if (num != null) { + assertTrue("Zipped number differs: list#" + list.getNumber() + "; number:" + list.getComment(), + String.valueOf(num).equals(list.getComment())); + } else { + assertTrue("Zipped number differs: too much for list where 1 must be: " + list.getNumber(), "1".equals(list.getComment())); + } + } + } + + private static class Convertor implements VcsCommittedListsZipper { + private final static Convertor ourInstance = new Convertor(); + + @Override + @NotNull + public Pair, List> groupLocations(@NotNull List in) { + RepositoryLocationGroup group = new RepositoryLocationGroup(""); + + for (RepositoryLocation location : in) { + group.add(location); + } + + return Pair.create(Collections.singletonList(group), Collections.emptyList()); + } + + @Override + @NotNull + public CommittedChangeList zip(@Nullable RepositoryLocationGroup group, @NotNull List lists) { + return create(lists.get(0).getNumber(), String.valueOf(lists.size())); + } + + @Override + public long getNumber(@NotNull CommittedChangeList list) { + return list.getNumber(); + } + } + + @NotNull + private static List list(@NotNull long... numbers) { + List result = new ArrayList<>(numbers.length); + + for (long number : numbers) { + result.add(create(number)); + } + + return result; + } + + @NotNull + private static CommittedChangeList create(long number) { + return create(number, "1"); + } + + @NotNull + private static CommittedChangeList create(long number, @NotNull String comment) { + return new CommittedChangeListImpl("", comment, "", number, null, Collections.emptyList()) { + @Override + public String toString() { + return getNumber() + " " + getComment(); + } + }; + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/ExternalChangesDetectionVcsTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/ExternalChangesDetectionVcsTest.java index a91fd91c28a3..4c9bef181096 100644 --- a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/ExternalChangesDetectionVcsTest.java +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/ExternalChangesDetectionVcsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/FilterDescendantFilesTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/FilterDescendantFilesTest.java new file mode 100644 index 000000000000..45f8d0bfd23a --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/FilterDescendantFilesTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.FilterDescendantVirtualFiles; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.testFramework.PlatformTestCase; +import junit.framework.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FilterDescendantFilesTest extends PlatformTestCase { + @Test + public void testSecondModuleSameLevelAsProject() throws Throwable { + final File tmpDir = createDir(new File(FileUtil.getTempDirectory()), "tmpDir"); + final File child1 = createDir(tmpDir, "child1"); + final File child2 = createDir(tmpDir, "child2"); + + final List list = convert(new File[]{child2, child2, child1}); + Assert.assertEquals(3, list.size()); + FilterDescendantVirtualFiles.filter(list); + Assert.assertEquals(2, list.size()); + } + + @Test + public void testUsual() throws Throwable { + File tmp = new File(FileUtil.getTempDirectory()); + final File tmpDir = createDir(tmp, "tmpDir"); + final File child1 = createDir(tmpDir, "child1"); + final File child2 = createDir(tmp, "child2"); + + final List list = convert(new File[]{tmpDir, child2, child1}); + Assert.assertEquals(3, list.size()); + FilterDescendantVirtualFiles.filter(list); + Assert.assertEquals(2, list.size()); + } + + private final List convert(final File[] files) { + final List result = new ArrayList<>(); + final LocalFileSystem lfs = LocalFileSystem.getInstance(); + for (File file : files) { + VirtualFile vf = lfs.findFileByIoFile(file); + if (vf == null) { + vf = lfs.refreshAndFindFileByIoFile(file); + } + if (vf != null) { + result.add(vf); + } + } + return result; + } + + private File createDir(final File parent, final String name) throws IOException { + final File result = new File(parent, name); + for (int i = 0; i < 100; i++) { + if (result.mkdirs()) break; + } + + myFilesToDelete.add(result); + ApplicationManager.getApplication().runWriteAction(() -> { + VirtualFileManager.getInstance().syncRefresh(); + }); + return result; + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockCommittedChangesProvider.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockCommittedChangesProvider.java new file mode 100644 index 000000000000..be51f0e1fa72 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockCommittedChangesProvider.java @@ -0,0 +1,257 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.*; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings; +import com.intellij.openapi.vcs.versionBrowser.ChangesBrowserSettingsEditor; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeListImpl; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.vcs.MockContentRevision; +import com.intellij.util.AsynchConsumer; +import com.intellij.vcsUtil.VcsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.*; + +/** + * @author yole + */ +public class MockCommittedChangesProvider implements CachingCommittedChangesProvider { + private final List myChangeLists = new ArrayList<>(); + private int myRefreshCount = 0; + + @NotNull + @Override + public ChangeBrowserSettings createDefaultSettings() { + return new ChangeBrowserSettings(); + } + + @Override + public ChangesBrowserSettingsEditor createFilterUI(final boolean showDateFilter) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public RepositoryLocation getLocationFor(FilePath root) { + return new DefaultRepositoryLocation(root.getPath()); + } + + @Override + public RepositoryLocation getLocationFor(final FilePath root, final String repositoryPath) { + return getLocationFor(root); + } + + @Nullable + @Override + public VcsCommittedListsZipper getZipper() { + return null; + } + + @Override + public List getCommittedChanges(ChangeBrowserSettings settings, RepositoryLocation location, final int maxCount) throws VcsException { + myRefreshCount++; + return myChangeLists; + } + + @Override + public void loadCommittedChanges(ChangeBrowserSettings settings, + RepositoryLocation location, + int maxCount, + AsynchConsumer consumer) + throws VcsException { + ++ myRefreshCount; + for (CommittedChangeListImpl changeList : myChangeLists) { + consumer.consume(changeList); + } + consumer.finished(); + } + + @Override + public Pair getOneList(VirtualFile file, VcsRevisionNumber number) throws VcsException { + ++ myRefreshCount; + return new Pair<>(myChangeLists.get(0), VcsUtil.getFilePath(file)); + } + + @Override + public RepositoryLocation getForNonLocal(VirtualFile file) { + return null; + } + + @Override + public boolean supportsIncomingChanges() { + return true; + } + + public int getRefreshCount() { + return myRefreshCount; + } + + @Override + public ChangeListColumn[] getColumns() { + return new ChangeListColumn[0]; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + @Nullable + public VcsCommittedViewAuxiliary createActions(final DecoratorManager manager, final RepositoryLocation location) { + return null; + } + + @Override + public int getUnlimitedCountValue() { + return 0; + } + + public CommittedChangeList registerChangeList(final String name, final Change... changes) { + final CommittedChangeListImpl list = createList(name, "user",name, new Date().getTime(), 1, changes); + myChangeLists.add(list); + return list; + } + + private CommittedChangeListImpl createList(final String name, final String author, final String comment, + final long date, final long number, final Change... changes) { + final Collection changeList = new ArrayList<>(); + Collections.addAll(changeList, changes); + return new CommittedChangeListImpl(name, comment, author, number, new Date(date), changeList); + } + + @Override + public int getFormatVersion() { + return 0; + } + + @Override + public void writeChangeList(final DataOutput stream, final CommittedChangeListImpl list) throws IOException { + stream.writeUTF(list.getName()); + stream.writeInt(list.getChanges().size()); + stream.writeUTF(list.getCommitterName()); + stream.writeUTF(list.getComment()); + stream.writeLong(list.getCommitDate().getTime()); + stream.writeLong(list.getNumber()); + + for(Change c: list.getChanges()) { + ContentRevision revision = c.getAfterRevision(); + if (revision == null) { + stream.writeByte(0); + revision = c.getBeforeRevision(); + } + else { + stream.writeByte(c.getBeforeRevision() != null ? 1 : 2); + } + VcsRevisionNumber.Int revisionNumber = (VcsRevisionNumber.Int) revision.getRevisionNumber(); + stream.writeUTF(revision.getFile().getIOFile().getPath()); + stream.writeInt(revisionNumber.getValue()); + } + } + + @Override + public CommittedChangeListImpl readChangeList(final RepositoryLocation location, final DataInput stream) throws IOException { + final String name = stream.readUTF(); + int changeCount = stream.readInt(); + final String author = stream.readUTF(); + final String comment = stream.readUTF(); + final long date = stream.readLong(); + final long number = stream.readLong(); + + final Change[] changes = new Change[changeCount]; + for(int i=0; i getIncomingFiles(final RepositoryLocation location) { + return null; + } + + @Override + public boolean refreshCacheByNumber() { + return false; + } + + @Override + public String getChangelistTitle() { + return null; + } + + @Override + public boolean isChangeLocallyAvailable(final FilePath filePath, + @Nullable final VcsRevisionNumber localRevision, final VcsRevisionNumber changeRevision, + final CommittedChangeListImpl changeList) { + return localRevision != null && localRevision.compareTo(changeRevision) >= 0; + } + + @Override + public boolean refreshIncomingWithCommitted() { + return false; + } + + public static Change createMockMovedChange(final String pathBefore, final String pathAfter, final int revision) { + final FilePath fullPath = VcsUtil.getFilePath(pathBefore, false); + final FilePath fullPath2 = VcsUtil.getFilePath(pathAfter, false); + + final ContentRevision beforeRevision = new MockContentRevision(fullPath, new VcsRevisionNumber.Int(revision-1)); + final ContentRevision afterRevision = new MockContentRevision(fullPath2, new VcsRevisionNumber.Int(revision)); + return new Change(beforeRevision, afterRevision); + } + + public static Change createMockChange(final String fullPathName, final int revision) { + final FilePath fullPath = VcsUtil.getFilePath(fullPathName, false); + final ContentRevision beforeRevision = new MockContentRevision(fullPath, new VcsRevisionNumber.Int(revision-1)); + final ContentRevision afterRevision = new MockContentRevision(fullPath, new VcsRevisionNumber.Int(revision)); + return new Change(beforeRevision, afterRevision); + } + + public static Change createMockDeleteChange(final String fullPathName, final int revision) { + final FilePath fullPath = VcsUtil.getFilePath(fullPathName, false); + final ContentRevision beforeRevision = new MockContentRevision(fullPath, new VcsRevisionNumber.Int(revision)); + return new Change(beforeRevision, null); + } + + public static Change createMockCreateChange(final String fullPathName, final int revision) { + final FilePath fullPath = VcsUtil.getFilePath(fullPathName, false); + final ContentRevision afterRevision = new MockContentRevision(fullPath, new VcsRevisionNumber.Int(revision)); + return new Change(null, afterRevision); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockDiffProvider.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockDiffProvider.java new file mode 100644 index 000000000000..ae07b467a9a3 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockDiffProvider.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.diff.DiffProvider; +import com.intellij.openapi.vcs.diff.ItemLatestState; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author yole + */ +public class MockDiffProvider implements DiffProvider { + private final Map myCurrentRevisionNumbers = new HashMap<>(); + + public void setCurrentRevision(VirtualFile file, VcsRevisionNumber number) { + myCurrentRevisionNumbers.put(file, number); + } + + @Override + @Nullable + public VcsRevisionNumber getCurrentRevision(VirtualFile file) { + return myCurrentRevisionNumbers.get(file); + } + + @Override + @Nullable + public ItemLatestState getLastRevision(VirtualFile virtualFile) { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public ContentRevision createFileContent(VcsRevisionNumber revisionNumber, VirtualFile selectedFile) { + throw new UnsupportedOperationException(); + } + + @Override + public ItemLatestState getLastRevision(FilePath filePath) { + throw new UnsupportedOperationException(); + } + + @Override + public VcsRevisionNumber getLatestCommittedRevision(VirtualFile vcsRoot) { + throw new UnsupportedOperationException(); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockRecordingPairProcessor.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockRecordingPairProcessor.java new file mode 100644 index 000000000000..45de066ce3bb --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/MockRecordingPairProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.util.PairProcessor; + +/** +* @author irengrig +*/ +public class MockRecordingPairProcessor implements PairProcessor { + private Integer myValue; + private String myKey; + + @Override + public boolean process(final String s, final Integer integer) { + myKey = s; + myValue = integer; + return true; + } + + public String getKey() { + return myKey; + } + + public Integer getValue() { + return myValue; + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/RequestsMergerTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/RequestsMergerTest.java new file mode 100644 index 000000000000..18ddf015de5a --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/RequestsMergerTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.util.RequestsMerger; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.Consumer; +import com.intellij.util.TimeoutUtil; +import com.intellij.util.concurrency.Semaphore; +import junit.framework.Assert; +import junit.framework.TestCase; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +public class RequestsMergerTest extends TestCase { + public void testManyIntoOne() throws Exception { + final MyTestVictim victim = new MyTestVictim(); + final RequestsMerger merger = new RequestsMerger(victim, victim); + + for (int i = 0; i < 50; i++) { + merger.request(); + if (i == 0) { + Assert.assertEquals(true, victim.isExecutionSubmitted()); + } + Assert.assertEquals(false, victim.isExecuted()); + } + } + + public void testStartsSecondWhenRequestInTheMiddle() throws Exception { + final MyDelayableTestVictim victim = new MyDelayableTestVictim(); + final RequestsMerger merger = new RequestsMerger(victim, victim); + + for (int i = 0; i < 20; i++) { + merger.request(); + if (i == 0) { + Assert.assertEquals(true, victim.isExecutionSubmitted()); + } + Assert.assertEquals(false, victim.isExecuted()); + } + + victim.doDelayedRun(); + synchronized (this) { + try { + wait(50); + } + catch (InterruptedException e) { + // + } + } + + for (int i = 0; i < 20; i++) { + merger.request(); + Assert.assertEquals(false, victim.isExecutionSubmitted()); + Assert.assertEquals(false, victim.isExecuted()); // not completed + } + + victim.allowRun(); + synchronized (this) { + try { + wait(50); + } + catch (InterruptedException e) { + // + } + } + Assert.assertEquals(true, victim.isExecuted()); // 1st finished + Assert.assertEquals(true, victim.isExecutionSubmitted()); // 2nd submitted + Assert.assertTrue(victim.isChildExited()); + victim.myThread.join(); + } + + public void testAfterRefreshIsCalled() throws Exception { + SimpleExecutor executor = null; + try { + executor = new SimpleExecutor(); + final SimpleRunnable runnable = new SimpleRunnable(); + final RequestsMerger merger = new RequestsMerger(runnable, executor); + + Assert.assertTrue(! runnable.isStarted()); + merger.request(); + TimeoutUtil.sleep(50); + // after a while first request started execution, so other requests merges into one next + for (int i = 0; i < 20; i++) { + merger.request(); + } + Assert.assertTrue(runnable.isStarted()); + Assert.assertTrue(! runnable.isFinished()); + Assert.assertEquals(0, runnable.getCnt()); + + // this will complete first request + runnable.letGo(); + TimeoutUtil.sleep(50); + // but there's also second one + Assert.assertTrue(runnable.isStarted()); + Assert.assertTrue(! runnable.isFinished()); + Assert.assertEquals(1, runnable.getCnt()); + // this will complete all 2. no more yet. + runnable.letGo(); + TimeoutUtil.sleep(50); + Assert.assertTrue(! runnable.isStarted()); + Assert.assertTrue(runnable.isFinished()); + Assert.assertEquals(2, runnable.getCnt()); + + // now lets test after-execution + final SimpleRunnable checker = new SimpleRunnable(); + // this will add waiter + 1 request + merger.waitRefresh(checker); + // still waiting + TimeoutUtil.sleep(50); + Assert.assertTrue(runnable.isStarted()); + Assert.assertTrue(! runnable.isFinished()); + Assert.assertEquals(2, runnable.getCnt()); + + Assert.assertTrue(! checker.isStarted()); + Assert.assertTrue(! checker.isFinished()); + Assert.assertEquals(0, checker.getCnt()); + // this will start runnable and start checker but not finish checker + runnable.letGo(); + TimeoutUtil.sleep(50); + Assert.assertTrue(! runnable.isStarted()); + Assert.assertTrue(runnable.isFinished()); + Assert.assertEquals(3, runnable.getCnt()); + + Assert.assertTrue(checker.isStarted()); + Assert.assertTrue(! checker.isFinished()); + Assert.assertEquals(0, checker.getCnt()); + checker.letGo(); + TimeoutUtil.sleep(50); + Assert.assertTrue(! checker.isStarted()); + Assert.assertTrue(checker.isFinished()); + Assert.assertEquals(1, checker.getCnt()); + } finally { + if (executor != null) { + executor.dispose(); + } + } + } + + private static class SimpleExecutor implements Consumer { + private final ExecutorService myExecutor; + + private SimpleExecutor() { + myExecutor = ConcurrencyUtil.newSingleThreadExecutor("req merge test"); + } + + @Override + public void consume(Runnable runnable) { + myExecutor.submit(runnable); + } + + public void dispose() throws InterruptedException { + myExecutor.shutdownNow(); + assertTrue(myExecutor.awaitTermination(100, TimeUnit.SECONDS)); + } + } + + private static class SimpleRunnable implements Runnable { + private int myCnt; + private boolean myStarted; + private boolean myFinished; + private final Semaphore mySemaphore; + private final Object myLock; + + private SimpleRunnable() { + mySemaphore = new Semaphore(); + myLock = new Object(); + } + + private int getCnt() { + synchronized (myLock) { + return myCnt; + } + } + + private boolean isStarted() { + synchronized (myLock) { + return myStarted; + } + } + + private boolean isFinished() { + synchronized (myLock) { + return myFinished; + } + } + + public void letGo() { + synchronized (myLock) { + mySemaphore.up(); + } + } + + @Override + public void run() { + synchronized (myLock) { + assert ! myStarted; + myStarted = true; + myFinished = false; + } + mySemaphore.down(); + mySemaphore.waitFor(); + synchronized (myLock) { + myStarted = false; + myFinished = true; + ++ myCnt; + } + } + } + + private static class MyDelayableTestVictim extends MyTestVictim { + private final Semaphore mySemaphore; + private volatile boolean myChildExited; + private Thread myThread; + + private MyDelayableTestVictim() { + mySemaphore = new Semaphore(); + } + + @Override + public void doDelayedRun() { + // another thread + final Semaphore local = new Semaphore(); + local.down(); + myThread = new Thread("req merge test") { + @Override + public void run() { + try { + local.up(); + myRunnable.run(); + } + finally { + myChildExited = true; + } + } + }; + myThread.start(); + local.waitFor(); + myExecutionSubmitted = false; // hack: to check further submissions + } + + @Override + public void run() { + mySemaphore.down(); + mySemaphore.waitFor(); + super.run(); + } + + public void allowRun() { + mySemaphore.up(); + } + + public boolean isChildExited() { + return myChildExited; + } + } + + private static class MyTestVictim implements Runnable, Consumer { + protected boolean myExecutionSubmitted; + private boolean myExecuted; + protected Runnable myRunnable; + + @Override + public void consume(Runnable runnable) { + Assert.assertFalse(myExecutionSubmitted); + myExecutionSubmitted = true; + myRunnable = runnable; + } + + @Override + public void run() { + myExecuted = true; + myExecutionSubmitted = false; + } + + public void doDelayedRun() { + myRunnable.run(); + } + + public boolean isExecutionSubmitted() { + return myExecutionSubmitted; + } + + public boolean isExecuted() { + return myExecuted; + } + + public void reset() { + myExecuted = false; + myExecutionSubmitted = false; + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/SelectionManagerTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/SelectionManagerTest.java new file mode 100644 index 000000000000..46d483b34927 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/SelectionManagerTest.java @@ -0,0 +1,257 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.ThrowableComputable; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.util.containers.Convertor; +import com.intellij.util.treeWithCheckedNodes.SelectionManager; +import com.intellij.util.treeWithCheckedNodes.TreeNodeState; +import junit.framework.Assert; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +/** + * @author irengrig + * Date: 2/7/11 + * Time: 1:17 PM + */ +public class SelectionManagerTest extends PlatformTestCase { + private FileStructure myFs; + private SelectionManager myCm; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ApplicationManager.getApplication().runWriteAction(new ThrowableComputable() { + @Override + public Void compute() throws IOException { + myFs = new FileStructure(getProject()); + return null; + } + }); + myCm = new SelectionManager(2, 10, MyConvertor.getInstance()); + } + + public void testSimple() throws Exception { + assertClear(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + afterMiddle1(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + assertClear(); + } + + public void testSimpleRemove() throws Exception { + assertClear(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + afterMiddle1(); + myCm.removeSelection(myFs.myMiddle1); + assertClear(); + } + + public void testCannotChangeChild() throws Exception { + assertClear(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + afterMiddle1(); + myCm.toggleSelection(myFs.getNode(myFs.myInner11)); + afterMiddle1(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + assertClear(); + + // and still can change child now + myCm.toggleSelection(myFs.getNode(myFs.myInner11)); + afterInner11(); + // back to clear + myCm.toggleSelection(myFs.getNode(myFs.myInner11)); + assertClear(); + } + + private void assertClear() { + assertNodeState(myFs.myParent, TreeNodeState.CLEAR, true); + } + + private void afterMiddle1() { + assertNodeState(myFs.myParent, TreeNodeState.HAVE_SELECTED_BELOW, false); + assertNodeState(myFs.myMiddle2, TreeNodeState.CLEAR, true); + assertNodeState(myFs.myMiddle1, TreeNodeState.SELECTED, false); + assertNodeState(myFs.myInner11, TreeNodeState.HAVE_SELECTED_ABOVE, true); + assertNodeState(myFs.myInner12, TreeNodeState.HAVE_SELECTED_ABOVE, true); + } + + private void afterInner11() { + assertNodeState(myFs.myParent, TreeNodeState.HAVE_SELECTED_BELOW, false); + assertNodeState(myFs.myMiddle2, TreeNodeState.CLEAR, true); + assertNodeState(myFs.myMiddle1, TreeNodeState.HAVE_SELECTED_BELOW, false); + assertNodeState(myFs.myInner12, TreeNodeState.CLEAR, true); + + assertNodeState(myFs.myInner11, TreeNodeState.SELECTED, false); + assertNodeState(myFs.myLeaf1, TreeNodeState.HAVE_SELECTED_ABOVE, false); + assertNodeState(myFs.myLeaf2, TreeNodeState.HAVE_SELECTED_ABOVE, false); + } + + public void testLimit() throws Exception { + assertClear(); + + myCm.toggleSelection(myFs.getNode(myFs.myInner11)); + myCm.toggleSelection(myFs.getNode(myFs.myInner12)); + + Runnable afterTwoMiddle = () -> { + assertNodeState(myFs.myParent, TreeNodeState.HAVE_SELECTED_BELOW, false); + assertNodeState(myFs.myMiddle2, TreeNodeState.CLEAR, true); + assertNodeState(myFs.myMiddle1, TreeNodeState.HAVE_SELECTED_BELOW, false); + + assertNodeState(myFs.myInner12, TreeNodeState.SELECTED, true); + + assertNodeState(myFs.myInner11, TreeNodeState.SELECTED, false); + assertNodeState(myFs.myLeaf1, TreeNodeState.HAVE_SELECTED_ABOVE, false); + assertNodeState(myFs.myLeaf2, TreeNodeState.HAVE_SELECTED_ABOVE, false); + }; + afterTwoMiddle.run(); + + // try third + myCm.toggleSelection(myFs.getNode(myFs.myInner21)); + // get same + afterTwoMiddle.run(); + + // take parent + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + afterMiddle1(); + // clear + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + assertClear(); + } + + public void testCanExpand() throws Exception { + assertClear(); + myCm.toggleSelection(myFs.getNode(myFs.myInner11)); + afterInner11(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + afterMiddle1(); + myCm.toggleSelection(myFs.getNode(myFs.myMiddle1)); + assertClear(); + } + + public void testTwoTrees() throws Exception { + final Map middle1map = myFs.createNodeMap(myFs.myMiddle1); + assertClear(); + myCm.toggleSelection(middle1map.get(myFs.myInner11)); + afterInner11(); // though selected in smaller subtree + myCm.toggleSelection(middle1map.get(myFs.myInner11)); + assertClear(); + + myCm.toggleSelection(middle1map.get(myFs.myInner11)); + afterInner11(); // though selected in smaller subtree + myCm.toggleSelection(middle1map.get(myFs.myMiddle1)); + afterMiddle1(); // though selected in smaller subtree + } + + + private void assertNodeState(@NotNull final VirtualFile vf, final TreeNodeState state, final boolean recursively) { + Assert.assertNotNull(myFs.getNode(vf)); + Assert.assertEquals(state, myCm.getState(myFs.getNode(vf))); + // not deep, ok recursion + if (recursively) { + for (VirtualFile child : vf.getChildren()) { + assertNodeState(child, state, true); + } + } + } + + private static class FileStructure { + private VirtualFile myParent; + private VirtualFile myMiddle1; + private VirtualFile myMiddle2; + private VirtualFile myInner11; + private VirtualFile myInner12; + private VirtualFile myInner21; + private VirtualFile myInner22; + private VirtualFile myLeaf1; + private VirtualFile myLeaf2; + + private Map myMap; + private final Project myProject; + + private FileStructure(final Project project) throws IOException { + myProject = project; + final VirtualFile baseDir = project.getBaseDir(); + + myParent = baseDir.createChildDirectory(this, "parent"); + myMiddle1 = myParent.createChildDirectory(this, "middle1"); + myMiddle2 = myParent.createChildDirectory(this, "middle2"); + + myInner11 = myMiddle1.createChildDirectory(this, "inner11"); + myInner12 = myMiddle1.createChildDirectory(this, "inner12"); + myInner21 = myMiddle2.createChildDirectory(this, "inner21"); + myInner22 = myMiddle2.createChildDirectory(this, "inner22"); + + myLeaf1 = myInner11.createChildDirectory(this, "leaf1"); + myLeaf2 = myInner11.createChildDirectory(this, "leaf2"); + + myMap = createNodeMap(myParent); + } + + public DefaultMutableTreeNode getNode(final VirtualFile vf) { + return myMap.get(vf); + } + + Map createNodeMap(final VirtualFile parentFile) { + Map result = new HashMap<>(); + final LinkedList queue = new LinkedList<>(); + queue.add(parentFile); + // for fictive node + DefaultMutableTreeNode parentNode = new DefaultMutableTreeNode(null); + while (! queue.isEmpty()) { + final VirtualFile file = queue.removeFirst(); + final DefaultMutableTreeNode node = new DefaultMutableTreeNode(file); + result.put(file, node); + + final DefaultMutableTreeNode parent = result.get(file.getParent()); + parentChild(parent == null ? parentNode : parent, node); + + queue.addAll(Arrays.asList(file.getChildren())); + } + return result; + } + + private void parentChild(final DefaultMutableTreeNode parent, final DefaultMutableTreeNode child) { + parent.add(child); + child.setParent(parent); + } + } + + private static class MyConvertor implements Convertor { + private final static MyConvertor ourInstance = new MyConvertor(); + + public static MyConvertor getInstance() { + return ourInstance; + } + + @Override + public VirtualFile convert(DefaultMutableTreeNode o) { + final Object userObject = o.getUserObject(); + return userObject instanceof VirtualFile ? (VirtualFile) userObject : null; + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsDirtyScopeTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsDirtyScopeTest.java new file mode 100644 index 000000000000..3ad1e55121b9 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsDirtyScopeTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.VcsDirectoryMapping; +import com.intellij.openapi.vcs.changes.VcsDirtyScopeImpl; +import com.intellij.openapi.vcs.changes.VcsDirtyScopeModifier; +import com.intellij.openapi.vcs.changes.VcsModifiableDirtyScope; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.vcs.FileBasedTest; +import com.intellij.util.Consumer; +import com.intellij.util.ui.UIUtil; +import com.intellij.vcsUtil.VcsUtil; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * @author irengrig + */ +public class VcsDirtyScopeTest extends FileBasedTest { + private MockAbstractVcs myVcs; + private ProjectLevelVcsManagerImpl myVcsManager; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + myVcs = new MockAbstractVcs(myProject); + myVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(myProject); + myVcsManager.registerVcs(myVcs); + myVcsManager.setDirectoryMapping(myProjectFixture.getProject().getBaseDir().getPath(), myVcs.getName()); + } + + @Override + @After + public void tearDown() throws Exception { + myVcsManager = null; + myVcs = null; + super.tearDown(); + } + + private static class Data { + private VirtualFile baseDir; + private VirtualFile dir1; + private VirtualFile dir2; + private VirtualFile dir3; + private VirtualFile dir4; + private VirtualFile innerDir1; + private VirtualFile innerDir2; + private List files; + } + + @Test + public void testVcsIterator() throws Exception { + final Data data = createData(); + final MockAbstractVcs another = new MockAbstractVcs(myProject, "ANOTHER"); + myVcsManager.registerVcs(another); + final List mappings = new ArrayList<>(myVcsManager.getDirectoryMappings()); + mappings.add(new VcsDirectoryMapping(data.dir1.getPath(), another.getKeyInstanceMethod().getName())); + myVcsManager.setDirectoryMappings(mappings); + + myVcsManager.iterateVcsRoot(myProject.getBaseDir(), path -> { + Assert.assertFalse(VfsUtil.isAncestor(data.dir1, path.getVirtualFile(), false)); + Assert.assertTrue(myVcsManager.getVcsFor(path).equals(myVcs)); + return true; + }); + } + + @Test + public void testAddRemove() throws Exception { + final Data data = createData(); + + final VcsDirtyScopeImpl scope = new VcsDirtyScopeImpl(new MockAbstractVcs(myProject), myProject); + scope.addDirtyDirRecursively(VcsUtil.getFilePath(data.dir1)); + scope.addDirtyDirRecursively(VcsUtil.getFilePath(data.dir3)); + scope.addDirtyDirRecursively(VcsUtil.getFilePath(data.dir4)); + for (VirtualFile file : data.files) { + scope.addDirtyFile(VcsUtil.getFilePath(file)); + } + + final Set set = new HashSet<>(); + set.add(data.dir1); + set.add(data.dir3); + set.add(data.dir4); + set.add(data.files.get(1)); + set.add(data.files.get(5)); + + final HashSet removed = new HashSet<>(set); + removeMarked(set, scope, virtualFile -> removed.remove(virtualFile)); + + Assert.assertTrue(scope.isEmpty()); + Assert.assertTrue(removed.isEmpty()); + } + + @Test + public void testRecursivelyDirtyDirectoriesUnderNonRecursively() throws Exception { + final Data data = createData(); + + final VcsDirtyScopeImpl scope = new VcsDirtyScopeImpl(new MockAbstractVcs(myProject), myProject); + + scope.addDirtyData(Arrays.asList(VcsUtil.getFilePath(data.dir1), VcsUtil.getFilePath(data.dir2)), + Collections.singletonList(VcsUtil.getFilePath(data.baseDir))); + final Set dirtyDirs = scope.getRecursivelyDirtyDirectories(); + final Set dirtyFiles = scope.getDirtyFilesNoExpand(); + + Assert.assertNotNull(dirtyDirs); + Assert.assertNotNull(dirtyFiles); + Assert.assertTrue(dirtyFiles.contains(VcsUtil.getFilePath(data.baseDir))); + Assert.assertTrue(dirtyDirs.contains(VcsUtil.getFilePath(data.dir1))); + Assert.assertTrue(dirtyDirs.contains(VcsUtil.getFilePath(data.dir2))); + } + + private Data createData() throws IOException { + final Data data = new Data(); + data.baseDir = myProjectFixture.getProject().getBaseDir(); + final IOException[] exc = new IOException[1]; + final File ioFile = new File(data.baseDir.getPath()); + final File[] files = ioFile.listFiles(); + for (File file : files) { + FileUtil.delete(file); + } + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + try { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + try { + data.dir1 = data.baseDir.createChildDirectory(this, "dir1"); + data.dir2 = data.baseDir.createChildDirectory(this, "dir2"); + data.dir3 = data.baseDir.createChildDirectory(this, "dir3"); + data.dir4 = data.baseDir.createChildDirectory(this, "dir4"); + + data.innerDir1 = data.dir1.createChildDirectory(this, "innerDir1"); + data.innerDir2 = data.dir2.createChildDirectory(this, "innerDir2"); + + final VirtualFile[] virtualFiles = {data.dir1, data.dir2, data.dir3, data.dir4, data.innerDir1, data.innerDir2}; + int i = 1; + data.files = new LinkedList<>(); + for (VirtualFile vf : virtualFiles) { + data.files.add(vf.createChildData(this, "f" + i + ".txt")); + ++i; + } + } + catch (IOException e) { + exc[0] = e; + } + } + }); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + + if (exc[0] != null) { + throw exc[0]; + } + return data; + } + + + private static void removeMarked(final Set ignored, + final VcsModifiableDirtyScope scope, + final Consumer listener) { + final VcsDirtyScopeModifier modifier = scope.getModifier(); + if (modifier != null) { + final Iterator filesIterator = modifier.getDirtyFilesIterator(); + while (filesIterator.hasNext()) { + final FilePath dirtyFile = filesIterator.next(); + if ((dirtyFile.getVirtualFile() != null) && ignored.contains(dirtyFile.getVirtualFile())) { + filesIterator.remove(); + listener.consume(dirtyFile.getVirtualFile()); + } + } + final Collection roots = modifier.getAffectedVcsRoots(); + for (VirtualFile root : roots) { + final Iterator dirIterator = modifier.getDirtyDirectoriesIterator(root); + while (dirIterator.hasNext()) { + final FilePath dir = dirIterator.next(); + if ((dir.getVirtualFile() != null) && ignored.contains(dir.getVirtualFile())) { + dirIterator.remove(); + listener.consume(dir.getVirtualFile()); + } + } + } + modifier.recheckDirtyKeys(); + } + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsExcludedFileProcessingTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsExcludedFileProcessingTest.java new file mode 100644 index 000000000000..7b1d9439fe90 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsExcludedFileProcessingTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestCase; +import com.intellij.testFramework.PsiTestUtil; +import org.junit.Before; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author nik + */ +public class VcsExcludedFileProcessingTest extends PlatformTestCase { + private ProjectLevelVcsManagerImpl myVcsManager; + private MockAbstractVcs myVcs; + + @Before + public void setUp() throws Exception { + super.setUp(); + + myVcs = new MockAbstractVcs(myProject); + myVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(myProject); + myVcsManager.registerVcs(myVcs); + } + + public void testFileUnderExcludedRoot() throws IOException { + VirtualFile root = getVirtualFile(createTempDir("content")); + myVcsManager.setDirectoryMapping(root.getPath(), myVcs.getName()); + PsiTestUtil.addContentRoot(myModule, root); + VirtualFile excludedDir = createChildDirectory(root, "excluded"); + VirtualFile excludedFile = createChildData(excludedDir, "a.txt"); + PsiTestUtil.addExcludedRoot(myModule, excludedDir); + + assertEquals(root, myVcsManager.getVcsRootFor(excludedFile)); + + final List processed = new ArrayList<>(); + myVcsManager.iterateVcsRoot(root, path -> { + processed.add(path.getVirtualFile()); + return true; + }); + assertTrue(processed.contains(excludedFile)); + } +} diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsFileWatchRequestManagementTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsFileWatchRequestManagementTest.java new file mode 100644 index 000000000000..90ef6d1910b7 --- /dev/null +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/changes/committed/VcsFileWatchRequestManagementTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2000-2017 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.changes.committed; + +import com.intellij.mock.MockLocalFileSystem; +import com.intellij.openapi.vcs.FileStatusManager; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.VcsDirectoryMapping; +import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vcs.impl.projectlevelman.FileWatchRequestsManager; +import com.intellij.openapi.vcs.impl.projectlevelman.NewMappings; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.testFramework.PlatformTestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * @author irengrig + */ +public class VcsFileWatchRequestManagementTest extends PlatformTestCase { + private static final String ourVcsName = "vcs"; + + private NewMappings myNewMappings; + private MyMockLocalFileSystem myMockLocalFileSystem; + + @Override + public void setUp() throws Exception { + super.setUp(); + + ProjectLevelVcsManagerImpl vcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(myProject); + myNewMappings = new NewMappings(myProject, vcsManager, FileStatusManager.getInstance(myProject)); + myMockLocalFileSystem = new MyMockLocalFileSystem(); + myNewMappings.setFileWatchRequestsManager(new FileWatchRequestsManager(myProject, myNewMappings, myMockLocalFileSystem)); + myNewMappings.activateActiveVcses(); + } + + public void testAdd() { + final String path = "/a/b/c"; + myMockLocalFileSystem.add(path); + + myNewMappings.setMapping("", ourVcsName); + myNewMappings.setMapping(path, ourVcsName); + // add twice -> nothing happens + myNewMappings.setMapping(path, ourVcsName); + } + + public void testAddRemove() { + final String path = "/a/b/c"; + + myMockLocalFileSystem.add(path); + myNewMappings.setMapping(path, ourVcsName); + + myMockLocalFileSystem.remove(path); + myNewMappings.removeDirectoryMapping(new VcsDirectoryMapping(path, ourVcsName)); + } + + public void testAddSwitch() { + final String path = "/a/b/c"; + myMockLocalFileSystem.add(path); + myNewMappings.setMapping(path, ourVcsName); + + myMockLocalFileSystem.add(path); + myMockLocalFileSystem.remove(path); + myNewMappings.setMapping(path, "scv"); + } + + public void testAddSwitchRemoveAdd() { + final String path = "/a/b/c"; + final String path2 = "/a1/b1/c1"; + myMockLocalFileSystem.add(path); + myMockLocalFileSystem.add(path2); + myNewMappings.setMapping(path, ourVcsName); + myNewMappings.setMapping(path2, ourVcsName); + + // switch + myMockLocalFileSystem.add(path); + myMockLocalFileSystem.remove(path); + myNewMappings.setMapping(path, "scv"); + + // remove + myMockLocalFileSystem.remove(path2); + myNewMappings.removeDirectoryMapping(new VcsDirectoryMapping(path2, ourVcsName)); + + // add back + myMockLocalFileSystem.add(path2); + myNewMappings.setMapping(path2, ourVcsName); + } + + public void testSets() { + final String path = "/a/b/c"; + final String path2 = "/a2/b2/c2"; + final String path3 = "/a3/b3/c3"; + final String path4 = "/a4/b4/c4"; + final String path5 = "/a5/b5/c5"; + + final String anotherVcs = "another"; + + myMockLocalFileSystem.add(path); + myMockLocalFileSystem.add(path2); + myMockLocalFileSystem.add(path3); + myMockLocalFileSystem.add(path4); + + myNewMappings.setDirectoryMappings(Arrays.asList(new VcsDirectoryMapping(path, ourVcsName), + new VcsDirectoryMapping(path2, ourVcsName), + new VcsDirectoryMapping(path3, anotherVcs), + new VcsDirectoryMapping(path4, anotherVcs))); + + // set another + myMockLocalFileSystem.remove(path2); + myMockLocalFileSystem.remove(path3); + myMockLocalFileSystem.remove(path4); + myMockLocalFileSystem.add(path5); + myNewMappings.setDirectoryMappings(Arrays.asList(new VcsDirectoryMapping(path, ourVcsName), + new VcsDirectoryMapping(path5, anotherVcs))); + } + + private static class MyMockLocalFileSystem extends MockLocalFileSystem { + private final Set myAdd; + private final Set myRemove; + + private MyMockLocalFileSystem() { + myAdd = new HashSet<>(); + myRemove = new HashSet<>(); + } + + @NotNull + @Override + public Set replaceWatchedRoots(@NotNull Collection watchRequests, + @Nullable Collection recursiveRoots, + @Nullable Collection flatRoots) { + for (WatchRequest watchRequest : watchRequests) { + assertTrue(myRemove.remove(watchRequest.getRootPath())); + } + + Set requests = new HashSet<>(); + + if (recursiveRoots != null) { + for (String rootPath : recursiveRoots) { + assertTrue(myAdd.remove(rootPath)); + requests.add(new MockKey(rootPath, true)); + } + } + + if (flatRoots != null) { + for (String rootPath : flatRoots) { + assertTrue(myAdd.remove(rootPath)); + requests.add(new MockKey(rootPath, false)); + } + } + + return requests; + } + + public void add(final String path) { + myAdd.add(path); + } + + public void remove(final String path) { + myRemove.add(path); + } + } + + // should be, as originals, compared by references + private static class MockKey implements LocalFileSystem.WatchRequest { + private final String myPath; + private final boolean myRecursively; + + public MockKey(String path, boolean recursively) { + myPath = path; + myRecursively = recursively; + } + + + @NotNull + @Override + public String getRootPath() { + return myPath; + } + + @Override + public boolean isToWatchRecursively() { + return myRecursively; + } + } +} \ No newline at end of file diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsIntegrationEnablerTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsIntegrationEnablerTest.java index 7728c1644789..9b2419402312 100644 --- a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsIntegrationEnablerTest.java +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsIntegrationEnablerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootDetectorTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootDetectorTest.java index 672f802a094c..50911d69b2c1 100644 --- a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootDetectorTest.java +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootDetectorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootErrorsFinderTest.java b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootErrorsFinderTest.java index a02200df2c49..b74ee2aa6c07 100644 --- a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootErrorsFinderTest.java +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootErrorsFinderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootProblemNotifierTest.kt b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootProblemNotifierTest.kt index be5196e04c8b..a606936d222e 100644 --- a/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootProblemNotifierTest.kt +++ b/platform/vcs-tests/testSrc/com/intellij/openapi/vcs/roots/VcsRootProblemNotifierTest.kt @@ -13,21 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Copyright 2000-2017 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.roots import com.intellij.openapi.extensions.Extensions diff --git a/platform/vcs-tests/testSrc/com/intellij/vcs/VcsDirtyScopeManagerTest.kt b/platform/vcs-tests/testSrc/com/intellij/vcs/VcsDirtyScopeManagerTest.kt index cd2ce83bb89d..be8aa81f2e53 100644 --- a/platform/vcs-tests/testSrc/com/intellij/vcs/VcsDirtyScopeManagerTest.kt +++ b/platform/vcs-tests/testSrc/com/intellij/vcs/VcsDirtyScopeManagerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt b/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt index 192e9563d6ca..dbd24283a18a 100644 --- a/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt +++ b/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/XValueHint.java b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/XValueHint.java index 3712fc0aebb5..1ed74cb299be 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/XValueHint.java +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/XValueHint.java @@ -40,6 +40,7 @@ import com.intellij.ui.SimpleColoredComponent; import com.intellij.ui.SimpleColoredText; import com.intellij.ui.SimpleTextAttributes; import com.intellij.util.Consumer; +import com.intellij.util.concurrency.EdtExecutorService; import com.intellij.xdebugger.XDebugSession; import com.intellij.xdebugger.XDebuggerUtil; import com.intellij.xdebugger.XSourcePosition; @@ -66,6 +67,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; +import java.util.concurrent.TimeUnit; /** * @author nik @@ -160,6 +162,14 @@ public class XValueHint extends AbstractValueHint { @Override protected void evaluateAndShowHint() { + EdtExecutorService.getScheduledExecutorInstance().schedule(() -> { + if (myCurrentHint == null) { + SimpleColoredComponent component = HintUtil.createInformationComponent(); + component.append(XDebuggerUIConstants.EVALUATING_EXPRESSION_MESSAGE); + showHint(component); + } + }, 200, TimeUnit.MILLISECONDS); + myEvaluator.evaluate(myExpression, new XEvaluationCallbackBase() { @Override public void evaluated(@NotNull final XValue result) { @@ -229,6 +239,9 @@ public class XValueHint extends AbstractValueHint { } private void showTree(@NotNull XValue value) { + if (myCurrentHint != null) { + myCurrentHint.hide(); + } XValueMarkers valueMarkers = ((XDebugSessionImpl)myDebugSession).getValueMarkers(); XDebuggerTreeCreator creator = new XDebuggerTreeCreator(myDebugSession.getProject(), myDebugSession.getDebugProcess().getEditorsProvider(), myDebugSession.getCurrentPosition(), valueMarkers); diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/AbstractValueHint.java b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/AbstractValueHint.java index a5d1ec34df7a..241b2f6c2895 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/AbstractValueHint.java +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/AbstractValueHint.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. @@ -70,7 +70,7 @@ public abstract class AbstractValueHint { private final Editor myEditor; private final ValueHintType myType; protected final Point myPoint; - private LightweightHint myCurrentHint; + protected LightweightHint myCurrentHint; private boolean myHintHidden; private TextRange myCurrentRange; private Runnable myHideRunnable; @@ -89,24 +89,26 @@ public abstract class AbstractValueHint { protected abstract void evaluateAndShowHint(); public boolean isKeepHint(Editor editor, Point point) { - if (myCurrentHint != null && myCurrentHint.canControlAutoHide()) { - return true; - } + return myType != ValueHintType.MOUSE_ALT_OVER_HINT; - if (myType == ValueHintType.MOUSE_ALT_OVER_HINT) { - return false; - } - else if (myType == ValueHintType.MOUSE_CLICK_HINT) { - if (myCurrentHint != null && myCurrentHint.isVisible()) { - return true; - } - } - else { - if (isInsideCurrentRange(editor, point)) { - return true; - } - } - return false; + //if (myCurrentHint != null && myCurrentHint.canControlAutoHide()) { + // return true; + //} + // + //if (myType == ValueHintType.MOUSE_ALT_OVER_HINT) { + // return false; + //} + //else if (myType == ValueHintType.MOUSE_CLICK_HINT) { + // if (myCurrentHint != null && myCurrentHint.isVisible()) { + // return true; + // } + //} + //else { + // if (isInsideCurrentRange(editor, point)) { + // return true; + // } + //} + //return false; } boolean isInsideCurrentRange(Editor editor, Point point) { diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/ValueLookupManager.java b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/ValueLookupManager.java index e5514b8ad4d1..11d5cff53267 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/ValueLookupManager.java +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/quick/common/ValueLookupManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. @@ -95,8 +95,13 @@ public class ValueLookupManager extends EditorMouseAdapter implements EditorMous } Point point = e.getMouseEvent().getPoint(); - if (myRequest != null && !myRequest.isKeepHint(editor, point)) { - hideHint(); + if (myRequest != null) { + if (myRequest.getType() == ValueHintType.MOUSE_CLICK_HINT) { + return; + } + else if (!myRequest.isKeepHint(editor, point)) { + hideHint(); + } } for (DebuggerSupport support : mySupports) { diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/ui/DebuggerUIUtil.java b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/ui/DebuggerUIUtil.java index 6b8e933fe047..5f8b4548af60 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/ui/DebuggerUIUtil.java +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/ui/DebuggerUIUtil.java @@ -132,7 +132,7 @@ public class DebuggerUIUtil { } public static void showValuePopup(@NotNull XFullValueEvaluator evaluator, @NotNull MouseEvent event, @NotNull Project project, @Nullable Editor editor) { - EditorTextField textArea = new TextViewer("Evaluating...", project); + EditorTextField textArea = new TextViewer(XDebuggerUIConstants.EVALUATING_EXPRESSION_MESSAGE, project); textArea.setBackground(HintUtil.getInformationColor()); final FullValueEvaluationCallbackImpl callback = new FullValueEvaluationCallbackImpl(textArea); diff --git a/plugins/cvs/cvs-plugin/src/com/intellij/cvsSupport2/impl/CvsServicesImpl.java b/plugins/cvs/cvs-plugin/src/com/intellij/cvsSupport2/impl/CvsServicesImpl.java index 1e090fb3ab48..771d65d4943b 100644 --- a/plugins/cvs/cvs-plugin/src/com/intellij/cvsSupport2/impl/CvsServicesImpl.java +++ b/plugins/cvs/cvs-plugin/src/com/intellij/cvsSupport2/impl/CvsServicesImpl.java @@ -94,13 +94,6 @@ public class CvsServicesImpl extends CvsServices { } - public void showDifferencesForFiles(CvsModule first, CvsModule second, Project project) throws Exception { - AbstractVcsHelper.getInstance(project).showDifferences( - createCvsVersionOn(first, project), - createCvsVersionOn(second, project), - new File(first.getPathInCvs())); - } - public String getScrambledPasswordForPServerCvsRoot(String cvsRoot) { return PServerLoginProvider.getInstance() .getScrambledPasswordForCvsRoot(cvsRoot); diff --git a/plugins/cvs/cvs-plugin/src/com/intellij/openapi/cvsIntegration/CvsServices.java b/plugins/cvs/cvs-plugin/src/com/intellij/openapi/cvsIntegration/CvsServices.java index 29fd7a5962b2..068b7953fd2d 100644 --- a/plugins/cvs/cvs-plugin/src/com/intellij/openapi/cvsIntegration/CvsServices.java +++ b/plugins/cvs/cvs-plugin/src/com/intellij/openapi/cvsIntegration/CvsServices.java @@ -31,7 +31,6 @@ public abstract class CvsServices { boolean allowFilesSelection, String title, String selectModulePageTitle); public abstract CvsRepository[] getConfiguredRepositories(); - public abstract void showDifferencesForFiles(CvsModule first, CvsModule second, Project project) throws Exception; public abstract String getScrambledPasswordForPServerCvsRoot(String cvsRoot); public abstract boolean saveRepository(CvsRepository repository); public abstract void openInEditor(Project project, CvsModule cvsFile); diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchType.kt b/plugins/git4idea/src/git4idea/branch/GitBranchType.kt new file mode 100644 index 000000000000..c07c6ef5bb1d --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitBranchType.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2016 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 git4idea.branch + +import com.intellij.dvcs.branch.BranchType + +enum class GitBranchType constructor(private val myName: String) : BranchType { + LOCAL("LOCAL"), REMOTE("REMOTE"); + + override fun getName(): String { + return myName + } +} diff --git a/plugins/git4idea/src/git4idea/config/GitVcsSettings.java b/plugins/git4idea/src/git4idea/config/GitVcsSettings.java index c3320e8a32ae..8521891cf8c6 100644 --- a/plugins/git4idea/src/git4idea/config/GitVcsSettings.java +++ b/plugins/git4idea/src/git4idea/config/GitVcsSettings.java @@ -15,10 +15,9 @@ */ package git4idea.config; -import com.intellij.dvcs.branch.BranchStorage; import com.intellij.dvcs.branch.DvcsBranchInfo; +import com.intellij.dvcs.branch.DvcsBranchSettings; import com.intellij.dvcs.branch.DvcsSyncSettings; -import com.intellij.dvcs.repo.Repository; import com.intellij.lifecycle.PeriodicalTasksCloser; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; @@ -30,10 +29,10 @@ import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.annotations.AbstractCollection; import com.intellij.util.xmlb.annotations.Attribute; +import com.intellij.util.xmlb.annotations.Property; import com.intellij.util.xmlb.annotations.Tag; import git4idea.GitRemoteBranch; import git4idea.GitUtil; -import git4idea.branch.GitBranchType; import git4idea.push.GitPushTagMode; import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; @@ -91,10 +90,8 @@ public class GitVcsSettings implements PersistentStateComponent PUSH_TARGETS = ContainerUtil.newArrayList(); - @Tag("favorite-branches") - public BranchStorage FAVORITE_BRANCHES = new BranchStorage(); - @Tag("excluded-from-favorite") - public BranchStorage EXCLUDED_FAVORITES = new BranchStorage(); + @Property(surroundWithTag = false, flat = true) + public DvcsBranchSettings FAVORITE_BRANCH_SETTINGS = new DvcsBranchSettings(); } public GitVcsSettings(GitVcsApplicationSettings appSettings) { @@ -294,28 +291,9 @@ public class GitVcsSettings implements PersistentStateComponent remotes = config.parseRemotes(); GitBranchState state = myReader.readState(remotes); Collection trackInfos = config.parseTrackInfos(state.getLocalBranches().keySet(), state.getRemoteBranches().keySet()); Collection submodules = new GitModulesFileReader().read(getSubmoduleFile()); + sw.report(); return new GitRepoInfo(state.getCurrentBranch(), state.getCurrentRevision(), state.getState(), remotes, state.getLocalBranches(), state.getRemoteBranches(), trackInfos, submodules); } diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitBranchManager.java b/plugins/git4idea/src/git4idea/ui/branch/GitBranchManager.java index f03f67b3d1c4..f77d4591ce9e 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitBranchManager.java +++ b/plugins/git4idea/src/git4idea/ui/branch/GitBranchManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 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. @@ -15,8 +15,8 @@ */ package git4idea.ui.branch; -import com.intellij.dvcs.branch.BranchStorage; -import com.intellij.dvcs.branch.DvcsBranchInfo; +import com.intellij.dvcs.branch.BranchType; +import com.intellij.dvcs.branch.DvcsBranchManager; import git4idea.branch.GitBranchType; import git4idea.config.GitVcsSettings; import git4idea.repo.GitRepository; @@ -24,60 +24,20 @@ import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - -import static com.intellij.util.containers.ContainerUtil.map2List; -import static com.intellij.util.containers.ContainerUtil.newArrayList; import static git4idea.log.GitRefManager.MASTER; import static git4idea.log.GitRefManager.ORIGIN_MASTER; -public class GitBranchManager { - @NotNull private final GitRepositoryManager myRepositoryManager; - @NotNull private final GitVcsSettings mySettings; - @NotNull public final BranchStorage myPredefinedFavoriteBranches = new BranchStorage(); +public class GitBranchManager extends DvcsBranchManager { public GitBranchManager(@NotNull GitRepositoryManager repositoryManager, @NotNull GitVcsSettings settings) { - myRepositoryManager = repositoryManager; - mySettings = settings; - for (GitBranchType type : GitBranchType.values()) { - myPredefinedFavoriteBranches.myBranches.put(type.toString(), constructDefaultBranchPredefinedList(type)); - } + super(repositoryManager, settings.getFavoriteBranchSettings(), GitBranchType.values()); } - @NotNull - private List constructDefaultBranchPredefinedList(GitBranchType type) { - List branchInfos = newArrayList(new DvcsBranchInfo("", getDefaultBranchName(type))); - branchInfos.addAll(map2List(myRepositoryManager.getRepositories(), - repository -> new DvcsBranchInfo(repository.getRoot().getPath(), getDefaultBranchName(type)))); - return branchInfos; - } - - @NotNull - private static String getDefaultBranchName(@NotNull GitBranchType type) { - return type == GitBranchType.LOCAL ? MASTER : ORIGIN_MASTER; - } - - public boolean isFavorite(@NotNull GitBranchType branchType, @Nullable GitRepository repository, @NotNull String branchName) { - if (mySettings.isFavorite(branchType, repository, branchName)) return true; - if (mySettings.isExcludedFromFavorites(branchType, repository, branchName)) return false; - return myPredefinedFavoriteBranches.contains(branchType.toString(), repository, branchName); - } - - public void setFavorite(@NotNull GitBranchType branchType, - @Nullable GitRepository repository, - @NotNull String branchName, - boolean shouldBeFavorite) { - if (shouldBeFavorite) { - mySettings.addToFavorites(branchType, repository, branchName); - mySettings.removeFromExcluded(branchType, repository, branchName); - } - else { - if (mySettings.isFavorite(branchType, repository, branchName)) { - mySettings.removeFromFavorites(branchType, repository, branchName); - } - else if (myPredefinedFavoriteBranches.contains(branchType.toString(), repository, branchName)) { - mySettings.excludedFromFavorites(branchType, repository, branchName); - } - } + @Nullable + @Override + protected String getDefaultBranchName(@NotNull BranchType type) { + if (type == GitBranchType.LOCAL) return MASTER; + if (type == GitBranchType.REMOTE) return ORIGIN_MASTER; + return null; } } diff --git a/plugins/git4idea/tests/git4idea/test/MockVcsHelper.kt b/plugins/git4idea/tests/git4idea/test/MockVcsHelper.kt index af52c850ddfb..4fb2d757f696 100644 --- a/plugins/git4idea/tests/git4idea/test/MockVcsHelper.kt +++ b/plugins/git4idea/tests/git4idea/test/MockVcsHelper.kt @@ -80,10 +80,6 @@ class MockVcsHelper(project: Project) : AbstractVcsHelper(project) { throw UnsupportedOperationException() } - override fun showDifferences(cvsVersionOn: VcsFileRevision, cvsVersionOn1: VcsFileRevision, file: File) { - throw UnsupportedOperationException() - } - override fun showChangesListBrowser(changelist: CommittedChangeList, title: String) { throw UnsupportedOperationException() } @@ -100,10 +96,6 @@ class MockVcsHelper(project: Project) : AbstractVcsHelper(project) { throw UnsupportedOperationException() } - override fun chooseCommittedChangeList(provider: CommittedChangesProvider, location: RepositoryLocation): T? { - throw UnsupportedOperationException() - } - override fun showRollbackChangesDialog(changes: List) { throw UnsupportedOperationException() } diff --git a/plugins/hg4idea/src/META-INF/plugin.xml b/plugins/hg4idea/src/META-INF/plugin.xml index 4f3514ba1556..c2939f43c355 100644 --- a/plugins/hg4idea/src/META-INF/plugin.xml +++ b/plugins/hg4idea/src/META-INF/plugin.xml @@ -37,6 +37,7 @@ serviceImplementation="org.zmlx.hg4idea.HgGlobalSettings"/> + diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgProjectSettings.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgProjectSettings.java index 29810853e7d8..f447081cd0d0 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgProjectSettings.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgProjectSettings.java @@ -12,6 +12,7 @@ // limitations under the License. package org.zmlx.hg4idea; +import com.intellij.dvcs.branch.DvcsBranchSettings; import com.intellij.dvcs.branch.DvcsSyncSettings; import com.intellij.lifecycle.PeriodicalTasksCloser; import com.intellij.openapi.components.PersistentStateComponent; @@ -20,6 +21,7 @@ import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.StoragePathMacros; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.changes.VcsAnnotationRefresher; +import com.intellij.util.xmlb.annotations.Property; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,6 +49,9 @@ public class HgProjectSettings implements PersistentStateComponent { public static final Topic REMOTE_TOPIC = new Topic<>("hg4idea.remote", HgUpdater.class); @@ -379,14 +382,13 @@ public class HgVcs extends AbstractVcs { } } - @Override - public boolean reportsIgnoredDirectories() { - return false; - } - @Override public List getCommitExecutors() { - return Arrays.asList(myCommitAndPushExecutor, myMqNewExecutor); + ArrayList commitExecutors = newArrayList(myCommitAndPushExecutor); + if (exists(HgUtil.getRepositoryManager(myProject).getRepositories(), r -> r.getRepositoryConfig().isMqUsed())) { + commitExecutors.add(myMqNewExecutor); + } + return commitExecutors; } @NotNull diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqAppliedPatchAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqAppliedPatchAction.java index cc3f413901b7..62b4def9e79a 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqAppliedPatchAction.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqAppliedPatchAction.java @@ -15,15 +15,12 @@ */ package org.zmlx.hg4idea.action.mq; -import com.intellij.openapi.util.Condition; import com.intellij.util.containers.ContainerUtil; import com.intellij.vcs.log.Hash; import org.jetbrains.annotations.NotNull; -import org.zmlx.hg4idea.HgNameWithHashInfo; -import org.zmlx.hg4idea.action.HgLogSingleCommitAction; import org.zmlx.hg4idea.repo.HgRepository; -public abstract class HgMqAppliedPatchAction extends HgLogSingleCommitAction { +public abstract class HgMqAppliedPatchAction extends HgMqLogAction { @Override protected boolean isEnabled(@NotNull HgRepository repository, @NotNull Hash commit) { @@ -31,11 +28,6 @@ public abstract class HgMqAppliedPatchAction extends HgLogSingleCommitAction { } public static boolean isAppliedPatch(@NotNull HgRepository repository, @NotNull final Hash hash) { - return ContainerUtil.exists(repository.getMQAppliedPatches(), new Condition() { - @Override - public boolean value(HgNameWithHashInfo info) { - return info.getHash().equals(hash); - } - }); + return ContainerUtil.exists(repository.getMQAppliedPatches(), info -> info.getHash().equals(hash)); } } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqLogAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqLogAction.java new file mode 100644 index 000000000000..f9d40095ea03 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgMqLogAction.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2017 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.zmlx.hg4idea.action.mq; + +import com.intellij.openapi.project.Project; +import com.intellij.vcs.log.Hash; +import org.jetbrains.annotations.NotNull; +import org.zmlx.hg4idea.action.HgLogSingleCommitAction; +import org.zmlx.hg4idea.repo.HgRepository; + +public abstract class HgMqLogAction extends HgLogSingleCommitAction { + + @Override + protected boolean isVisible(@NotNull Project project, @NotNull HgRepository repository, @NotNull Hash hash) { + return repository.getRepositoryConfig().isMqUsed() && super.isVisible(project, repository, hash); + } + + @Override + protected boolean isEnabled(@NotNull HgRepository repository, @NotNull Hash commit) { + return repository.getRepositoryConfig().isMqUsed() && super.isEnabled(repository, commit); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgQImportFromLogAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgQImportFromLogAction.java index d2321c99a2d3..9bc3cf34744e 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgQImportFromLogAction.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgQImportFromLogAction.java @@ -17,11 +17,10 @@ package org.zmlx.hg4idea.action.mq; import com.intellij.vcs.log.Hash; import org.jetbrains.annotations.NotNull; -import org.zmlx.hg4idea.action.HgLogSingleCommitAction; import org.zmlx.hg4idea.command.mq.HgQImportCommand; import org.zmlx.hg4idea.repo.HgRepository; -public class HgQImportFromLogAction extends HgLogSingleCommitAction { +public class HgQImportFromLogAction extends HgMqLogAction { @Override protected void actionPerformed(@NotNull HgRepository repository, @NotNull Hash commit) { String revisionHash = commit.asString(); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgShowUnAppliedPatchesAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgShowUnAppliedPatchesAction.java index 792921448dde..4c115b242c54 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgShowUnAppliedPatchesAction.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/mq/HgShowUnAppliedPatchesAction.java @@ -42,7 +42,7 @@ public class HgShowUnAppliedPatchesAction extends HgAbstractGlobalSingleRepoActi @Override public void update(AnActionEvent e) { HgRepository repository = HgActionUtil.getSelectedRepositoryFromEvent(e); - e.getPresentation().setEnabledAndVisible(repository != null); + e.getPresentation().setEnabledAndVisible(repository != null && repository.getRepositoryConfig().isMqUsed()); } public static void showUnAppliedPatches(@NotNull Project project, @NotNull HgRepository selectedRepo) { diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchManager.java b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchManager.java new file mode 100644 index 000000000000..19d99c11037d --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchManager.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2017 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.zmlx.hg4idea.branch; + +import com.intellij.dvcs.branch.BranchType; +import com.intellij.dvcs.branch.DvcsBranchManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.HgProjectSettings; +import org.zmlx.hg4idea.log.HgRefManager; +import org.zmlx.hg4idea.repo.HgRepository; +import org.zmlx.hg4idea.repo.HgRepositoryManager; + +public class HgBranchManager extends DvcsBranchManager { + public HgBranchManager(@NotNull HgRepositoryManager repositoryManager, @NotNull HgProjectSettings settings) { + super(repositoryManager, settings.getFavoriteBranchSettings(), HgBranchType.values()); + } + + @Nullable + @Override + protected String getDefaultBranchName(@NotNull BranchType type) { + return type == HgBranchType.BRANCH ? HgRefManager.DEFAULT : null; + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopup.java b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopup.java index ca8cb19b167a..f6f1f6ddf933 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopup.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopup.java @@ -19,11 +19,14 @@ import com.intellij.dvcs.DvcsUtil; import com.intellij.dvcs.branch.DvcsBranchPopup; import com.intellij.dvcs.repo.AbstractRepositoryManager; import com.intellij.dvcs.ui.RootAction; +import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Conditions; +import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.zmlx.hg4idea.HgProjectSettings; @@ -33,6 +36,15 @@ import org.zmlx.hg4idea.util.HgUtil; import javax.swing.*; import java.util.List; +import java.util.Objects; + +import static com.intellij.dvcs.branch.DvcsBranchPopup.MyMoreIndex.DEFAULT_NUM; +import static com.intellij.dvcs.branch.DvcsBranchPopup.MyMoreIndex.MAX_NUM; +import static com.intellij.dvcs.ui.BranchActionGroupPopup.wrapWithMoreActionIfNeeded; +import static com.intellij.dvcs.ui.BranchActionUtil.FAVORITE_BRANCH_COMPARATOR; +import static com.intellij.dvcs.ui.BranchActionUtil.getNumOfTopShownBranches; +import static java.util.stream.Collectors.toList; + /** *

@@ -44,6 +56,9 @@ import java.util.List; */ public class HgBranchPopup extends DvcsBranchPopup { private static final String DIMENSION_SERVICE_KEY = "Hg.Branch.Popup"; + static final String SHOW_ALL_BRANCHES_KEY = "Hg.Branch.Popup.ShowAllBranches"; + static final String SHOW_ALL_BOOKMARKS_KEY = "Hg.Branch.Popup.ShowAllBookmarks"; + static final String SHOW_ALL_REPOSITORIES = "Hg.Branch.Popup.ShowAllRepositories"; /** * @param currentRepository Current repository, which means the repository of the currently open or selected file. @@ -54,14 +69,7 @@ public class HgBranchPopup extends DvcsBranchPopup { HgProjectSettings hgProjectSettings = ServiceManager.getService(project, HgProjectSettings.class); HgMultiRootBranchConfig hgMultiRootBranchConfig = new HgMultiRootBranchConfig(manager.getRepositories()); - Condition preselectActionCondition = new Condition() { - @Override - public boolean value(AnAction action) { - return false; - } - }; - return new HgBranchPopup(currentRepository, manager, hgMultiRootBranchConfig, hgProjectSettings, - preselectActionCondition); + return new HgBranchPopup(currentRepository, manager, hgMultiRootBranchConfig, hgProjectSettings, Conditions.alwaysFalse()); } private HgBranchPopup(@NotNull HgRepository currentRepository, @@ -88,35 +96,45 @@ public class HgBranchPopup extends DvcsBranchPopup { popupGroup.addAll(createRepositoriesActions()); popupGroup.addSeparator("Common Branches"); - for (String branch : myMultiRootBranchConfig.getLocalBranchNames()) { - List repositories = filterRepositoriesNotOnThisBranch(branch, allRepositories); - if (!repositories.isEmpty()) { - popupGroup.add(new HgCommonBranchActions(myProject, repositories, branch)); - } - } + List branchActions = + myMultiRootBranchConfig.getLocalBranchNames().stream() + .map(b -> createLocalBranchActions(allRepositories, b, false)) + .filter(Objects::nonNull).collect(toList()); + wrapWithMoreActionIfNeeded(myProject, popupGroup, ContainerUtil.sorted(branchActions, FAVORITE_BRANCH_COMPARATOR), + getNumOfTopShownBranches(branchActions), SHOW_ALL_BRANCHES_KEY, true); + popupGroup.addSeparator("Common Bookmarks"); - for (String branch : ((HgMultiRootBranchConfig)myMultiRootBranchConfig).getBookmarkNames()) { - List repositories = filterRepositoriesNotOnThisBranch(branch, allRepositories); - if (!repositories.isEmpty()) { - popupGroup.add(new HgBranchPopupActions.BookmarkActions(myProject, repositories, branch)); - } - } + List bookmarkActions = ((HgMultiRootBranchConfig)myMultiRootBranchConfig).getBookmarkNames().stream() + .map(bm -> createLocalBranchActions(allRepositories, bm, true)) + .filter(Objects::nonNull).collect(toList()); + wrapWithMoreActionIfNeeded(myProject, popupGroup, ContainerUtil.sorted(bookmarkActions, FAVORITE_BRANCH_COMPARATOR), + getNumOfTopShownBranches(bookmarkActions), SHOW_ALL_BOOKMARKS_KEY, true); + } + + @Nullable + private HgCommonBranchActions createLocalBranchActions(List allRepositories, String name, boolean isBookmark) { + List repositories = filterRepositoriesNotOnThisBranch(name, allRepositories); + if (repositories.isEmpty()) return null; + return isBookmark + ? new HgBranchPopupActions.BookmarkActions(myProject, repositories, name) + : new HgBranchPopupActions.BranchActions(myProject, repositories, name); } @NotNull protected DefaultActionGroup createRepositoriesActions() { DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); popupGroup.addSeparator("Repositories"); - for (HgRepository repository : DvcsUtil.sortRepositories(myRepositoryManager.getRepositories())) { - popupGroup.add(new RootAction<>(repository, highlightCurrentRepo() ? myCurrentRepository : null, - new HgBranchPopupActions(repository.getProject(), repository).createActions(), - HgUtil.getDisplayableBranchOrBookmarkText(repository))); - } + List rootActions = DvcsUtil.sortRepositories(myRepositoryManager.getRepositories()).stream() + .map(repo -> new RootAction<>(repo, highlightCurrentRepo() ? myCurrentRepository : null, + new HgBranchPopupActions(repo.getProject(), repo).createActions(), + HgUtil.getDisplayableBranchOrBookmarkText(repo))).collect(toList()); + wrapWithMoreActionIfNeeded(myProject, popupGroup, rootActions, rootActions.size() > MAX_NUM ? DEFAULT_NUM : MAX_NUM, + SHOW_ALL_REPOSITORIES); return popupGroup; } protected void fillPopupWithCurrentRepositoryActions(@NotNull DefaultActionGroup popupGroup, @Nullable DefaultActionGroup actions) { - popupGroup.addAll(new HgBranchPopupActions(myProject, myCurrentRepository).createActions(actions, myRepoTitleInfo)); + popupGroup.addAll(new HgBranchPopupActions(myProject, myCurrentRepository).createActions(actions, myRepoTitleInfo, true)); } } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopupActions.java b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopupActions.java index 9288bd47b491..b80579503082 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopupActions.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchPopupActions.java @@ -55,6 +55,10 @@ import org.zmlx.hg4idea.util.HgUtil; import java.util.*; +import static com.intellij.dvcs.ui.BranchActionGroupPopup.wrapWithMoreActionIfNeeded; +import static com.intellij.dvcs.ui.BranchActionUtil.FAVORITE_BRANCH_COMPARATOR; +import static com.intellij.dvcs.ui.BranchActionUtil.getNumOfTopShownBranches; +import static java.util.stream.Collectors.toList; import static org.zmlx.hg4idea.util.HgUtil.getNewBranchNameFromUser; import static org.zmlx.hg4idea.util.HgUtil.getSortedNamesWithoutHashes; @@ -69,10 +73,10 @@ public class HgBranchPopupActions { } ActionGroup createActions() { - return createActions(null, ""); + return createActions(null, "", false); } - ActionGroup createActions(@Nullable DefaultActionGroup toInsert, @NotNull String repoInfo) { + ActionGroup createActions(@Nullable DefaultActionGroup toInsert, @NotNull String repoInfo, boolean firstLevelGroup) { DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); popupGroup.addAction(new HgNewBranchAction(myProject, Collections.singletonList(myRepository), myRepository)); popupGroup.addAction(new HgNewBookmarkAction(Collections.singletonList(myRepository), myRepository)); @@ -83,24 +87,25 @@ public class HgBranchPopupActions { } popupGroup.addSeparator("Bookmarks" + repoInfo); - List bookmarkNames = getSortedNamesWithoutHashes(myRepository.getBookmarks()); - String currentBookmark = myRepository.getCurrentBookmark(); - for (String bookmark : bookmarkNames) { - AnAction bookmarkAction = new BookmarkActions(myProject, Collections.singletonList(myRepository), bookmark); - if (bookmark.equals(currentBookmark)) { - bookmarkAction.getTemplatePresentation().setIcon(PlatformIcons.CHECK_ICON); - } - popupGroup.add(bookmarkAction); - } + List bookmarkActions = getSortedNamesWithoutHashes(myRepository.getBookmarks()).stream() + .map(bm -> new BookmarkActions(myProject, Collections.singletonList(myRepository), bm)) + .collect(toList()); + // if there are only a few local favorites -> show all; for remotes it's better to show only favorites; + wrapWithMoreActionIfNeeded(myProject, popupGroup, ContainerUtil.sorted(bookmarkActions, FAVORITE_BRANCH_COMPARATOR), + getNumOfTopShownBranches(bookmarkActions), firstLevelGroup ? HgBranchPopup.SHOW_ALL_BOOKMARKS_KEY : null, + firstLevelGroup); + //only opened branches have to be shown popupGroup.addSeparator("Branches" + repoInfo); - List branchNamesList = new ArrayList<>(myRepository.getOpenedBranches());//only opened branches have to be shown - Collections.sort(branchNamesList); - for (String branch : branchNamesList) { - if (!branch.equals(myRepository.getCurrentBranch())) { // don't show current branch in the list - popupGroup.add(new HgCommonBranchActions(myProject, Collections.singletonList(myRepository), branch)); - } - } + List branchActions = + myRepository.getOpenedBranches().stream() + .sorted() + .filter(b -> !b.equals(myRepository.getCurrentBranch())) + .map(b -> new BranchActions(myProject, Collections.singletonList(myRepository), b)) + .collect(toList()); + wrapWithMoreActionIfNeeded(myProject, popupGroup, ContainerUtil.sorted(branchActions, FAVORITE_BRANCH_COMPARATOR), + getNumOfTopShownBranches(branchActions), firstLevelGroup ? HgBranchPopup.SHOW_ALL_BRANCHES_KEY : null, + firstLevelGroup); return popupGroup; } @@ -269,13 +274,22 @@ public class HgBranchPopupActions { } } + static class BranchActions extends HgCommonBranchActions { + BranchActions(@NotNull Project project, @NotNull List repositories, @NotNull String branchName) { + super(project, repositories, branchName, HgBranchType.BRANCH); + } + } + /** * Actions available for bookmarks. */ static class BookmarkActions extends HgCommonBranchActions { BookmarkActions(@NotNull Project project, @NotNull List repositories, @NotNull String branchName) { - super(project, repositories, branchName); + super(project, repositories, branchName, HgBranchType.BOOKMARK); + if (myRepositories.size() == 1 && branchName.equals(myRepositories.get(0).getCurrentBookmark())) { + getTemplatePresentation().setIcon(PlatformIcons.CHECK_ICON); + } } @NotNull diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchType.kt b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchType.kt new file mode 100644 index 000000000000..62951aba6e40 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgBranchType.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2017 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.zmlx.hg4idea.branch + +import com.intellij.dvcs.branch.BranchType + +enum class HgBranchType constructor(private val myName: String) : BranchType { + BRANCH("BRANCH"), BOOKMARK("BOOKMARK"); + + override fun getName(): String { + return myName + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgCommonBranchActions.java b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgCommonBranchActions.java index 81ce78d72823..0f7f393f7de2 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgCommonBranchActions.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/branch/HgCommonBranchActions.java @@ -15,9 +15,11 @@ */ package org.zmlx.hg4idea.branch; +import com.intellij.dvcs.repo.Repository; import com.intellij.dvcs.ui.BranchActionGroup; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.update.UpdatedFiles; @@ -32,16 +34,40 @@ import java.util.List; public class HgCommonBranchActions extends BranchActionGroup { @NotNull protected final Project myProject; - @NotNull protected String myBranchName; - @NotNull List myRepositories; + @NotNull private final HgBranchManager myBranchManager; + @NotNull protected final String myBranchName; + @NotNull protected final List myRepositories; + @Nullable private final HgBranchType myBranchType; HgCommonBranchActions(@NotNull Project project, @NotNull List repositories, @NotNull String branchName) { + this(project, repositories, branchName, null); + } + + HgCommonBranchActions(@NotNull Project project, + @NotNull List repositories, + @NotNull String branchName, + @Nullable HgBranchType branchType) { myProject = project; myBranchName = branchName; myRepositories = repositories; + myBranchManager = ServiceManager.getService(project, HgBranchManager.class); getTemplatePresentation().setText(myBranchName, false); // no mnemonics - getTemplatePresentation().setIcon(null); - getTemplatePresentation().setHoveredIcon(null); + myBranchType = branchType; + setFavorite(myBranchManager.isFavorite(myBranchType, chooseRepository(myRepositories), myBranchName)); + hideIconForUnnamedHeads(); + } + + private void hideIconForUnnamedHeads() { + if (myBranchType == null) { + getTemplatePresentation().setIcon(null); + getTemplatePresentation().setHoveredIcon(null); + } + } + + @Nullable + private static Repository chooseRepository(@NotNull List repositories) { + assert !repositories.isEmpty(); + return repositories.size() > 1 ? null : repositories.get(0); } @NotNull @@ -53,6 +79,12 @@ public class HgCommonBranchActions extends BranchActionGroup { }; } + @Override + public void toggle() { + super.toggle(); + myBranchManager.setFavorite(myBranchType, chooseRepository(myRepositories), myBranchName, isFavorite()); + } + private static class MergeAction extends HgBranchAbstractAction { public MergeAction(@NotNull Project project, diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgShowConfigCommand.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgShowConfigCommand.java index f8b85b234421..fd8a83322103 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgShowConfigCommand.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgShowConfigCommand.java @@ -39,7 +39,7 @@ public class HgShowConfigCommand { final HgCommandExecutor executor = new HgCommandExecutor(project); executor.setSilent(true); //force override debug option while initialize hg configs - HgCommandResult result = executor.executeInCurrentThread(repo, "showconfig", Arrays.asList("--config", "ui.debug=false")); + HgCommandResult result = executor.executeInCurrentThread(repo, "showconfig", Arrays.asList("--config", "ui.debug=false"), true); if (result == null) { return Collections.emptyMap(); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgCommandExecutor.java b/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgCommandExecutor.java index be54fc0cc5bb..596b02fbb472 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgCommandExecutor.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgCommandExecutor.java @@ -49,7 +49,9 @@ import java.util.List; public class HgCommandExecutor { protected static final Logger LOG = Logger.getInstance(HgCommandExecutor.class.getName()); - private static final List DEFAULT_OPTIONS = Arrays.asList("--config", "ui.merge=internal:merge"); + + // Other parts of the plugin count on the availability of the MQ extension, so make sure it is enabled + private static final List DEFAULT_OPTIONS = Arrays.asList("--config", "extensions.mq=", "--config", "ui.merge=internal:merge"); protected final Project myProject; protected final HgVcs myVcs; @@ -95,15 +97,14 @@ public class HgCommandExecutor { myOutputAlwaysSuppressed = outputAlwaysSuppressed; } - public void execute(@Nullable final VirtualFile repo, @NotNull final String operation, @Nullable final List arguments, + public void execute(@Nullable final VirtualFile repo, + @NotNull final String operation, + @Nullable final List arguments, @Nullable final HgCommandResultHandler handler) { - HgUtil.executeOnPooledThread(new Runnable() { - @Override - public void run() { - HgCommandResult result = executeInCurrentThread(repo, operation, arguments); - if (handler != null) { - handler.process(result); - } + HgUtil.executeOnPooledThread(() -> { + HgCommandResult result = executeInCurrentThread(repo, operation, arguments); + if (handler != null) { + handler.process(result); } }, myProject); } @@ -111,10 +112,17 @@ public class HgCommandExecutor { public HgCommandResult executeInCurrentThread(@Nullable final VirtualFile repo, @NotNull final String operation, @Nullable final List arguments) { - HgCommandResult result = executeInCurrentThreadAndLog(repo, operation, arguments); + return executeInCurrentThread(repo, operation, arguments, false); + } + + public HgCommandResult executeInCurrentThread(@Nullable final VirtualFile repo, + @NotNull final String operation, + @Nullable final List arguments, + boolean ignoreDefaultOptions) { + HgCommandResult result = executeInCurrentThreadAndLog(repo, operation, arguments, ignoreDefaultOptions); if (HgErrorUtil.isUnknownEncodingError(result)) { setCharset(Charset.forName("utf8")); - result = executeInCurrentThreadAndLog(repo, operation, arguments); + result = executeInCurrentThreadAndLog(repo, operation, arguments, ignoreDefaultOptions); } return result; } @@ -122,10 +130,11 @@ public class HgCommandExecutor { @Nullable private HgCommandResult executeInCurrentThreadAndLog(@Nullable final VirtualFile repo, @NotNull final String operation, - @Nullable final List arguments) { + @Nullable final List arguments, + boolean ignoreDefaultOptions) { if (myProject == null || myProject.isDisposed() || myVcs == null) return null; - ShellCommand shellCommand = createShellCommandWithArgs(repo, operation, arguments); + ShellCommand shellCommand = createShellCommandWithArgs(repo, operation, arguments, ignoreDefaultOptions); try { long startTime = System.currentTimeMillis(); LOG.debug(String.format("hg %s started", operation)); @@ -153,7 +162,10 @@ public class HgCommandExecutor { } @NotNull - private ShellCommand createShellCommandWithArgs(@Nullable VirtualFile repo, @NotNull String operation, @Nullable List arguments) { + private ShellCommand createShellCommandWithArgs(@Nullable VirtualFile repo, + @NotNull String operation, + @Nullable List arguments, + boolean ignoreDefaultOptions) { logCommand(operation, arguments); @@ -164,11 +176,9 @@ public class HgCommandExecutor { cmdLine.add(repo.getPath()); } - // Other parts of the plugin count on the availability of the MQ extension, so make sure it is enabled - cmdLine.add("--config"); - cmdLine.add("extensions.mq="); - - cmdLine.addAll(DEFAULT_OPTIONS); + if (!ignoreDefaultOptions) { + cmdLine.addAll(DEFAULT_OPTIONS); + } cmdLine.add(operation); if (arguments != null && arguments.size() != 0) { cmdLine.addAll(arguments); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgRemoteCommandExecutor.java b/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgRemoteCommandExecutor.java index 3269950b73ad..cc73d96b692f 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgRemoteCommandExecutor.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/execution/HgRemoteCommandExecutor.java @@ -49,7 +49,7 @@ public class HgRemoteCommandExecutor extends HgCommandExecutor { @Nullable final List arguments) { - HgCommandResult result = executeInCurrentThread(repo, operation, arguments, false); + HgCommandResult result = executeRemoteCommandInCurrentThread(repo, operation, arguments, false); if (!myIgnoreAuthorizationRequest && HgErrorUtil.isAuthorizationError(result)) { if (HgErrorUtil.hasAuthorizationInDestinationPath(myDestination)) { new HgCommandResultNotifier(myProject) @@ -57,16 +57,16 @@ public class HgRemoteCommandExecutor extends HgCommandExecutor { "Please, update your .hg/hgrc file."); return null; } - result = executeInCurrentThread(repo, operation, arguments, true); + result = executeRemoteCommandInCurrentThread(repo, operation, arguments, true); } return result; } @Nullable - private HgCommandResult executeInCurrentThread(@Nullable final VirtualFile repo, - @NotNull final String operation, - @Nullable final List arguments, - boolean forceAuthorization) { + private HgCommandResult executeRemoteCommandInCurrentThread(@Nullable final VirtualFile repo, + @NotNull final String operation, + @Nullable final List arguments, + boolean forceAuthorization) { PassReceiver passReceiver = new PassReceiver(myProject, forceAuthorization, myIgnoreAuthorizationRequest, myState); SocketServer passServer = new SocketServer(passReceiver); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/log/HgRefManager.java b/plugins/hg4idea/src/org/zmlx/hg4idea/log/HgRefManager.java index 0c1015af108d..5db316f8df36 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/log/HgRefManager.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/log/HgRefManager.java @@ -62,7 +62,7 @@ public class HgRefManager implements VcsLogRefManager { } }; - private static final String DEFAULT = "default"; + public static final String DEFAULT = "default"; // @NotNull private final RepositoryManager myRepositoryManager; diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgConfig.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgConfig.java index 9396d6b94ec8..cd3d6f821cb8 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgConfig.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgConfig.java @@ -11,9 +11,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; -/** - * @author Nadya Zabrodina - */ public class HgConfig { @NotNull private final Map> myConfigMap; @@ -54,4 +51,9 @@ public class HgConfig { Map pathOptions = myConfigMap.get("paths"); return pathOptions != null ? pathOptions.values() : Collections.emptyList(); } + + public boolean isMqUsed() { + String value = getNamedConfig("extensions", "mq"); + return (value != null && !value.trim().startsWith("!")); + } } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java index c0ef05f03b73..0351a7e3b490 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java @@ -162,7 +162,12 @@ final class HgRepositoryUpdater implements Disposable, BulkFileListener { myUpdateQueue.queue(new MyUpdater("hgrepositoryUpdate")); } if (configHgrcChanged) { - myUpdateConfigQueue.queue(new MyUpdater("hgconfigUpdate")); + myUpdateConfigQueue.queue(new MyUpdater("hgconfigUpdate"){ + @Override + public void run() { + myRepository.updateConfig(); + } + }); } if (dirstateFileChanged || hgIgnoreChanged) { myRepository.getLocalIgnoredHolder().startRescan(); diff --git a/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java index 625861257172..a1ef07aea56b 100644 --- a/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java +++ b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java @@ -77,10 +77,6 @@ public class HgMockVcsHelper extends AbstractVcsHelper { public void showAnnotation(FileAnnotation annotation, VirtualFile file, AbstractVcs vcs) { } - @Override - public void showDifferences(VcsFileRevision cvsVersionOn, VcsFileRevision cvsVersionOn1, File file) { - } - @Override public void showChangesListBrowser(CommittedChangeList changelist, @Nls String title) { } @@ -104,12 +100,6 @@ public class HgMockVcsHelper extends AbstractVcsHelper { public void showWhatDiffersBrowser(@Nullable Component parent, Collection changes, @Nls String title) { } - @Override - public T chooseCommittedChangeList(@NotNull CommittedChangesProvider provider, - RepositoryLocation location) { - return null; - } - @Override public void openCommittedChangesTab(AbstractVcs vcs, VirtualFile root, ChangeBrowserSettings settings, int maxCount, String title) { } diff --git a/plugins/junit/src/com/intellij/execution/junit/TestObject.java b/plugins/junit/src/com/intellij/execution/junit/TestObject.java index c6100e7c6998..b850c90ecccf 100644 --- a/plugins/junit/src/com/intellij/execution/junit/TestObject.java +++ b/plugins/junit/src/com/intellij/execution/junit/TestObject.java @@ -169,13 +169,18 @@ public abstract class TestObject extends JavaTestFrameworkRunnableState 0; + } + public static boolean isJUnit5(@Nullable Module module, @Nullable SourceScope sourceScope, Project project) { return JUnitUtil.isJUnit5(getScopeForJUnit(module, sourceScope, project), project); } diff --git a/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnit5EngineDetector.java b/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnit5EngineDetector.java new file mode 100644 index 000000000000..324204c52817 --- /dev/null +++ b/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnit5EngineDetector.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2017 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.rt.execution.junit; + +import java.util.Iterator; +import java.util.ServiceLoader; + +public class JUnit5EngineDetector { + + public static boolean hasCustomEngine() { + try { + Iterator iterator = ServiceLoader.load(Class.forName("org.junit.platform.engine.TestEngine")).iterator(); + while (iterator.hasNext()) { + Object engine = iterator.next(); + String engineClassName = engine.getClass().getName(); + if (!"org.junit.jupiter.engine.JupiterTestEngine".equals(engineClassName) && + !"org.junit.vintage.engine.VintageTestEngine".equals(engineClassName)) { + return true; + } + } + return false; + } + catch (Throwable e) { + return false; + } + } +} diff --git a/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnitStarter.java b/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnitStarter.java index 332f409872aa..240d1d6bcc33 100644 --- a/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnitStarter.java +++ b/plugins/junit_rt/src/com/intellij/rt/execution/junit/JUnitStarter.java @@ -177,8 +177,13 @@ public class JUnitStarter { public static boolean isJUnit5Preferred() { final String useJUnit5 = System.getProperty(JUNIT5_KEY); - final Boolean boolValue = useJUnit5 == null ? null : Boolean.valueOf(useJUnit5); - return boolValue != null && boolValue.booleanValue(); + if (useJUnit5 == null) { + return JUnit5EngineDetector.hasCustomEngine(); + } + else { + final Boolean boolValue = Boolean.valueOf(useJUnit5); + return boolValue != null && boolValue.booleanValue(); + } } public static boolean checkVersion(String[] args, PrintStream printStream) { diff --git a/python/build/plugin-list.txt b/python/build/plugin-list.txt index 26753835d061..093e0b09d024 100644 --- a/python/build/plugin-list.txt +++ b/python/build/plugin-list.txt @@ -14,4 +14,5 @@ rest python-rest ipnb editorconfig -settings-repository \ No newline at end of file +settings-repository +yaml \ No newline at end of file diff --git a/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java index c6305c1090e3..1b45fb108668 100644 --- a/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/coursecreator/stepik/CCStepicConnector.java @@ -15,9 +15,15 @@ import com.intellij.openapi.vfs.VirtualFileFilter; import com.jetbrains.edu.learning.StudySerializationUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; -import com.jetbrains.edu.learning.courseFormat.*; +import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder; +import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Lesson; import com.jetbrains.edu.learning.courseFormat.tasks.Task; -import com.jetbrains.edu.learning.stepic.*; +import com.jetbrains.edu.learning.stepic.EduStepicAuthorizedClient; +import com.jetbrains.edu.learning.stepic.EduStepicNames; +import com.jetbrains.edu.learning.stepic.StepicUser; +import com.jetbrains.edu.learning.stepic.StepicWrappers; import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; @@ -135,7 +141,6 @@ public class CCStepicConnector { task.setLesson(lesson); task.setName(EduNames.PYCHARM_ADDITIONAL); task.setIndex(1); - task.setText(EduNames.PYCHARM_ADDITIONAL); for (VirtualFile file : files) { try { if (file != null) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java b/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java index 3f528eb32d84..c782034265d9 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudySerializationUtils.java @@ -8,6 +8,7 @@ import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; @@ -441,11 +442,15 @@ public class StudySerializationUtils { public static final String TASK_LIST = "task_list"; public static final String TASK_FILES = "task_files"; public static final String FILES = "files"; + public static final String TESTS = "test"; + public static final String TEXTS = "text"; public static final String HINTS = "hints"; public static final String SUBTASK_INFOS = "subtask_infos"; public static final String FORMAT_VERSION = "format_version"; public static final String INDEX = "index"; public static final String TASK_TYPE = "task_type"; + public static final String NAME = "name"; + public static final String LAST_SUBTASK = "last_subtask_index"; private Json() { } @@ -518,9 +523,11 @@ public class StudySerializationUtils { switch (version) { case 1: stepOptionsJson = convertToSecondVersion(stepOptionsJson); - // uncomment for future versions - //case 2: - // stepOptionsJson = convertToThirdVersion(stepOptionsJson); + case 2: + stepOptionsJson = convertToThirdVersion(stepOptionsJson); + // uncomment for future versions + //case 3: + // stepOptionsJson = convertToFourthVersion(stepOptionsJson); } convertSubtaskInfosToMap(stepOptionsJson); StepicWrappers.StepOptions stepOptions = @@ -530,6 +537,37 @@ public class StudySerializationUtils { return stepOptions; } + private JsonObject convertToThirdVersion(JsonObject stepOptionsJson) { + if (!stepOptionsJson.has(LAST_SUBTASK)) return stepOptionsJson; + final int lastSubtaskIndex = stepOptionsJson.get(LAST_SUBTASK).getAsInt(); + if (lastSubtaskIndex == 0) return stepOptionsJson; + final JsonArray tests = stepOptionsJson.getAsJsonArray(TESTS); + if (tests.size() > 0) { + final JsonObject fileWrapper = tests.get(0).getAsJsonObject(); + if (fileWrapper.has(NAME)) { + replaceWithSubtask(fileWrapper); + } + } + final JsonArray descriptions = stepOptionsJson.getAsJsonArray(TEXTS); + if (descriptions.size() > 0) { + final JsonObject fileWrapper = descriptions.get(0).getAsJsonObject(); + if (fileWrapper.has(NAME)) { + replaceWithSubtask(fileWrapper); + } + } + return stepOptionsJson; + } + + private void replaceWithSubtask(JsonObject fileWrapper) { + final String file = fileWrapper.get(NAME).getAsString(); + final String extension = FileUtilRt.getExtension(file); + final String name = FileUtil.getNameWithoutExtension(file); + if (!name.contains(EduNames.SUBTASK_MARKER)) { + fileWrapper.remove(NAME); + fileWrapper.add(NAME, new JsonPrimitive(name + "_subtask0." + extension)); + } + } + private static JsonObject convertSubtaskInfosToMap(JsonObject stepOptionsJson) { final JsonArray files = stepOptionsJson.getAsJsonArray(FILES); if (files != null) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java b/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java index 7266ee194759..e2e06e430d59 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/StudyUtils.java @@ -64,7 +64,10 @@ import com.jetbrains.edu.learning.core.EduAnswerPlaceholderDeleteHandler; import com.jetbrains.edu.learning.core.EduAnswerPlaceholderPainter; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; -import com.jetbrains.edu.learning.courseFormat.*; +import com.jetbrains.edu.learning.courseFormat.AnswerPlaceholder; +import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.ChoiceTask; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; @@ -492,7 +495,7 @@ public class StudyUtils { return null; } final Course course = task.getLesson().getCourse(); - String text = task.getText() != null ? task.getText() : getTaskTextByTaskName(task, taskDirectory); + String text = task.getTaskDescription() != null ? task.getTaskDescription() : getTaskTextByTaskName(task, taskDirectory); if (text == null) return null; text = convertToHtml(text); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java index ed7f0c414ab5..d5b56e06dd01 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseFormat/tasks/Task.java @@ -5,11 +5,9 @@ import com.google.gson.annotations.SerializedName; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.xmlb.XmlSerializer; import com.intellij.util.xmlb.annotations.Transient; -import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.courseFormat.*; import com.jetbrains.edu.learning.stepic.EduStepicConnector; @@ -37,7 +35,6 @@ public class Task implements StudyItem { @SerializedName("task_files") @Expose public Map taskFiles = new HashMap<>(); - private String text; protected Map testsText = new HashMap<>(); protected Map taskTexts = new HashMap<>(); @@ -73,14 +70,6 @@ public class Task implements StudyItem { this.name = name; } - public String getText() { - return text; - } - - public void setText(final String text) { - this.text = text; - } - @Override public int getIndex() { return myIndex; @@ -166,20 +155,11 @@ public class Task implements StudyItem { return null; } - @NotNull - public String getTaskText(@NotNull final Project project) { - if (!StringUtil.isEmptyOrSpaces(text)) return text; - final VirtualFile taskDir = getTaskDir(project); - if (taskDir != null) { - final VirtualFile file = StudyUtils.findTaskDescriptionVirtualFile(project, taskDir); - if (file == null) return ""; - final Document document = FileDocumentManager.getInstance().getDocument(file); - if (document != null) { - return document.getImmutableCharSequence().toString(); - } + public String getTaskDescription() { + if (!taskTexts.isEmpty()) { + return taskTexts.get(EduNames.TASK_HTML); } - - return ""; + return null; } @NotNull @@ -207,7 +187,7 @@ public class Task implements StudyItem { if (myIndex != task.myIndex) return false; if (name != null ? !name.equals(task.name) : task.name != null) return false; if (taskFiles != null ? !taskFiles.equals(task.taskFiles) : task.taskFiles != null) return false; - if (text != null ? !text.equals(task.text) : task.text != null) return false; + if (taskTexts != null ? !taskTexts.equals(task.taskTexts) : task.taskTexts != null) return false; if (testsText != null ? !testsText.equals(task.testsText) : task.testsText != null) return false; return true; @@ -218,7 +198,7 @@ public class Task implements StudyItem { int result = name != null ? name.hashCode() : 0; result = 31 * result + myIndex; result = 31 * result + (taskFiles != null ? taskFiles.hashCode() : 0); - result = 31 * result + (text != null ? text.hashCode() : 0); + result = 31 * result + (taskTexts != null ? taskTexts.hashCode() : 0); result = 31 * result + (testsText != null ? testsText.hashCode() : 0); return result; } @@ -273,7 +253,6 @@ public class Task implements StudyItem { setStatus(task.getStatus()); setStepId(task.getStepId()); taskFiles = task.getTaskFiles(); - setText(task.getText()); testsText = task.getTestsText(); taskTexts = task.getTaskTexts(); setLesson(task.getLesson()); diff --git a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java index fa240e513afb..58c82ec9b24f 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/courseGeneration/StudyProjectGenerator.java @@ -28,12 +28,12 @@ import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.courseFormat.Lesson; -import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.TaskFile; +import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.editor.StudyEditor; import com.jetbrains.edu.learning.statistics.EduUsagesCollector; -import com.jetbrains.edu.learning.courseFormat.CourseInfo; import com.jetbrains.edu.learning.stepic.EduStepicConnector; import com.jetbrains.edu.learning.stepic.StepicUpdateSettings; import com.jetbrains.edu.learning.stepic.StepicUser; @@ -291,24 +291,7 @@ public class StudyProjectGenerator { } } createFiles(taskDirectory, task.getTestsText()); - if (task.getTaskTexts().isEmpty()) { - createTaskHtml(task, taskDirectory); - return; - } - task.setText(null); createFiles(taskDirectory, task.getTaskTexts()); - - } - - private static void createTaskHtml(@NotNull Task task, @NotNull File taskDirectory) { - final File taskText = new File(taskDirectory, EduNames.TASK_HTML); - FileUtil.createIfDoesntExist(taskText); - try { - FileUtil.writeToFile(taskText, task.getText()); - } - catch (IOException e) { - LOG.error(e); - } } private static void createFiles(@NotNull File taskDirectory, Map files) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java index d675c983cb84..db1dc541e2ce 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduAdaptiveStepicConnector.java @@ -174,7 +174,8 @@ public class EduAdaptiveStepicConnector { private static Task getTheoryTaskFromStep(@NotNull String lessonName, @NotNull StepicWrappers.Step block, int stepId) { final Task task = new TheoryTask(lessonName); task.setStepId(stepId); - task.setText(block.text); + + task.addTaskText(EduNames.TASK_HTML, block.text); createMockTaskFile(task, "# this is a theory task. You can use this editor as a playground"); return task; @@ -186,7 +187,7 @@ public class EduAdaptiveStepicConnector { int stepId, int userId) { final ChoiceTask task = new ChoiceTask(lessonName); task.setStepId(stepId); - task.setText(block.text); + task.addTaskText(EduNames.TASK_HTML, block.text); final StepicWrappers.AdaptiveAttemptWrapper.Attempt attempt = getAttemptForStep(stepId, userId); if (attempt != null) { @@ -338,7 +339,7 @@ public class EduAdaptiveStepicConnector { } final StudyToolWindow window = StudyUtils.getStudyToolWindow(project); if (window != null) { - window.setTaskText(StudyUtils.wrapTextToDisplayLatex(unsolvedTask.getText()), unsolvedTask.getTaskDir(project), project); + window.setTaskText(StudyUtils.wrapTextToDisplayLatex(unsolvedTask.getTaskDescription()), unsolvedTask.getTaskDir(project), project); } StudyNavigator.navigateToTask(project, lessonName, taskName); } @@ -425,32 +426,28 @@ public class EduAdaptiveStepicConnector { int lessonID) { final Task task = new CodeTask(name); task.setStepId(lessonID); - task.setText(step.text); + task.setStatus(StudyStatus.Unchecked); + final StringBuilder taskDescription = new StringBuilder(step.text); if (step.options.samples != null) { - final StringBuilder builder = new StringBuilder(); + taskDescription.append("
"); for (List sample : step.options.samples) { if (sample.size() == 2) { - builder.append("Sample Input:
"); - builder.append(StringUtil.replace(sample.get(0), "\n", "
")); - builder.append("
"); - builder.append("Sample Output:
"); - builder.append(StringUtil.replace(sample.get(1), "\n", "
")); - builder.append("

"); + taskDescription.append("Sample Input:
"); + taskDescription.append(StringUtil.replace(sample.get(0), "\n", "
")); + taskDescription.append("
"); + taskDescription.append("Sample Output:
"); + taskDescription.append(StringUtil.replace(sample.get(1), "\n", "
")); + taskDescription.append("

"); } } - task.setText(task.getText() + "
" + builder.toString()); } if (step.options.executionMemoryLimit != null && step.options.executionTimeLimit != null) { - String builder = "Memory limit: " + - step.options.executionMemoryLimit + " Mb" + - "
" + - "Time limit: " + - step.options.executionTimeLimit + "s" + - "

"; - task.setText(task.getText() + builder); + taskDescription.append("
").append("Memory limit: ").append(step.options.executionMemoryLimit).append(" Mb").append("
") + .append("Time limit: ").append(step.options.executionTimeLimit).append("s").append("

"); } + task.addTaskText(EduNames.TASK_HTML, taskDescription.toString()); if (step.options.test != null) { for (StepicWrappers.FileWrapper wrapper : step.options.test) { diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java index c9d046918286..b4e2f8b4107e 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/EduStepicConnector.java @@ -9,7 +9,11 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.jetbrains.edu.learning.courseFormat.*; +import com.jetbrains.edu.learning.core.EduNames; +import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import org.apache.http.HttpEntity; @@ -288,7 +292,7 @@ public class EduStepicConnector { task.addTaskText(wrapper.name, wrapper.text); } } else { - task.setText(block.text); + task.addTaskText(EduNames.TASK_HTML, block.text); } task.taskFiles = new HashMap<>(); // TODO: it looks like we don't need taskFiles as map anymore diff --git a/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java b/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java index b2285b1c7683..b5a2065c3135 100644 --- a/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java +++ b/python/educational-core/src/com/jetbrains/edu/learning/stepic/StepicWrappers.java @@ -12,7 +12,10 @@ import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.core.EduNames; import com.jetbrains.edu.learning.core.EduUtils; -import com.jetbrains.edu.learning.courseFormat.*; +import com.jetbrains.edu.learning.courseFormat.Course; +import com.jetbrains.edu.learning.courseFormat.CourseInfo; +import com.jetbrains.edu.learning.courseFormat.Lesson; +import com.jetbrains.edu.learning.courseFormat.TaskFile; import com.jetbrains.edu.learning.courseFormat.tasks.Task; import com.jetbrains.edu.learning.courseFormat.tasks.TaskWithSubtasks; import org.apache.commons.codec.binary.Base64; @@ -39,7 +42,6 @@ public class StepicWrappers { public static Step fromTask(Project project, @NotNull final Task task) { final Step step = new Step(); - step.text = task.getTaskText(project); step.source = StepOptions.fromTask(project, task); return step; } @@ -55,7 +57,7 @@ public class StepicWrappers { @Expose Integer executionTimeLimit; @Expose CodeTemplatesWrapper codeTemplates; @SerializedName("format_version") - @Expose public int formatVersion = 2; + @Expose public int formatVersion = 3; @SerializedName("last_subtask_index") @Expose int lastSubtaskIndex = 0; diff --git a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java index 6e666a39040b..a24db818717f 100644 --- a/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java +++ b/python/educational-python/Edu-Python/src/com/jetbrains/edu/learning/PyStudyTestRunner.java @@ -31,7 +31,7 @@ public class PyStudyTestRunner extends StudyTestRunner { Course course = myTask.getLesson().getCourse(); PyEduPluginConfigurator configurator = new PyEduPluginConfigurator(); String testsFileName = configurator.getTestFileName(); - if (myTask instanceof TaskWithSubtasks && ((TaskWithSubtasks)myTask).getActiveSubtaskIndex() != 0) { + if (myTask instanceof TaskWithSubtasks) { testsFileName = FileUtil.getNameWithoutExtension(testsFileName); int index = ((TaskWithSubtasks)myTask).getActiveSubtaskIndex(); testsFileName += EduNames.SUBTASK_MARKER + index + "." + FileUtilRt.getExtension(configurator.getTestFileName()); diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineElementTypes.java b/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineElementTypes.java index 81c73d057785..c0230a875986 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineElementTypes.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineElementTypes.java @@ -18,6 +18,12 @@ public interface CommandLineElementTypes { IElementType LITERAL_STARTS_FROM_SYMBOL = new IElementType("LITERAL_STARTS_FROM_SYMBOL", null); IElementType LONG_OPTION_NAME_TOKEN = new IElementType("LONG_OPTION_NAME_TOKEN", null); IElementType SHORT_OPTION_NAME_TOKEN = new IElementType("SHORT_OPTION_NAME_TOKEN", null); + IElementType SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT = new IElementType("SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT", null); + IElementType SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER = new IElementType("SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER", null); + IElementType SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL = new IElementType("SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL", null); + IElementType SPACED_LITERAL_STARTS_FROM_DIGIT = new IElementType("SPACED_LITERAL_STARTS_FROM_DIGIT", null); + IElementType SPACED_LITERAL_STARTS_FROM_LETTER = new IElementType("SPACED_LITERAL_STARTS_FROM_LETTER", null); + IElementType SPACED_LITERAL_STARTS_FROM_SYMBOL = new IElementType("SPACED_LITERAL_STARTS_FROM_SYMBOL", null); class Factory { public static PsiElement createElement(ASTNode node) { diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineParser.java b/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineParser.java index 84fa8f433273..51f532412eb3 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineParser.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/CommandLineParser.java @@ -43,7 +43,9 @@ public class CommandLineParser implements PsiParser, LightPsiParser { } /* ********************************************************** */ - // LITERAL_STARTS_FROM_LETTER | LITERAL_STARTS_FROM_DIGIT | LITERAL_STARTS_FROM_SYMBOL + // LITERAL_STARTS_FROM_LETTER | LITERAL_STARTS_FROM_DIGIT | LITERAL_STARTS_FROM_SYMBOL | + // SPACED_LITERAL_STARTS_FROM_LETTER | SPACED_LITERAL_STARTS_FROM_DIGIT | SPACED_LITERAL_STARTS_FROM_SYMBOL | + // SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER | SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT | SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL public static boolean argument(PsiBuilder b, int l) { if (!recursion_guard_(b, l, "argument")) return false; boolean r; @@ -51,6 +53,12 @@ public class CommandLineParser implements PsiParser, LightPsiParser { r = consumeToken(b, LITERAL_STARTS_FROM_LETTER); if (!r) r = consumeToken(b, LITERAL_STARTS_FROM_DIGIT); if (!r) r = consumeToken(b, LITERAL_STARTS_FROM_SYMBOL); + if (!r) r = consumeToken(b, SPACED_LITERAL_STARTS_FROM_LETTER); + if (!r) r = consumeToken(b, SPACED_LITERAL_STARTS_FROM_DIGIT); + if (!r) r = consumeToken(b, SPACED_LITERAL_STARTS_FROM_SYMBOL); + if (!r) r = consumeToken(b, SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER); + if (!r) r = consumeToken(b, SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT); + if (!r) r = consumeToken(b, SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL); exit_section_(b, l, m, r, false, null); return r; } diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.java b/python/gen/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.java index c7e305136d32..66dc6bbf61f0 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.java @@ -1,9 +1,12 @@ /* The following code was generated by JFlex 1.7.0-SNAPSHOT tweaked for IntelliJ platform */ package com.jetbrains.commandInterface.commandLine; -import com.intellij.lexer.*; + +import com.intellij.lexer.FlexLexer; import com.intellij.psi.tree.IElementType; -import static com.intellij.psi.TokenType.*; + +import static com.intellij.psi.TokenType.BAD_CHARACTER; +import static com.intellij.psi.TokenType.WHITE_SPACE; import static com.jetbrains.commandInterface.commandLine.CommandLineElementTypes.*; @@ -109,47 +112,47 @@ public class _CommandLineLexer implements FlexLexer { /* The ZZ_CMAP_A table has 3088 entries */ static final char ZZ_CMAP_A[] = zzUnpackCMap( - "\11\0\5\1\22\0\1\1\14\0\1\7\2\6\12\11\1\5\2\0\1\13\3\0\32\4\1\0\1\5\2\0\1"+ - "\3\13\4\3\0\1\12\6\0\1\1\12\0\1\1\11\0\1\2\12\0\1\2\4\0\1\2\5\0\27\2\1\0\12"+ - "\2\4\0\14\2\16\0\5\2\7\0\1\2\1\0\1\2\1\0\5\2\1\0\2\2\2\0\4\2\1\0\1\2\6\0\1"+ - "\2\1\0\3\2\1\0\1\2\1\0\4\2\1\0\23\2\1\0\13\2\10\0\6\2\1\0\26\2\2\0\1\2\6\0"+ - "\10\2\10\0\13\2\5\0\3\2\15\0\12\10\4\0\6\2\1\0\1\2\17\0\2\2\7\0\2\2\12\10"+ - "\3\2\2\0\2\2\1\0\16\2\15\0\11\2\13\0\1\2\16\0\12\10\6\2\4\0\2\2\4\0\1\2\5"+ - "\0\6\2\4\0\1\2\11\0\1\2\3\0\1\2\7\0\11\2\7\0\5\2\17\0\26\2\3\0\1\2\2\0\1\2"+ - "\7\0\12\2\4\0\12\10\1\2\4\0\10\2\2\0\2\2\2\0\26\2\1\0\7\2\1\0\1\2\3\0\4\2"+ - "\3\0\1\2\20\0\1\2\15\0\2\2\1\0\1\2\5\0\6\2\4\0\2\2\1\0\2\2\1\0\2\2\1\0\2\2"+ - "\17\0\4\2\1\0\1\2\7\0\12\10\2\0\3\2\20\0\11\2\1\0\2\2\1\0\2\2\1\0\5\2\3\0"+ - "\1\2\2\0\1\2\30\0\1\2\13\0\10\2\2\0\1\2\3\0\1\2\1\0\6\2\3\0\3\2\1\0\4\2\3"+ - "\0\2\2\1\0\1\2\1\0\2\2\3\0\2\2\3\0\3\2\3\0\14\2\13\0\10\2\1\0\2\2\10\0\3\2"+ - "\5\0\4\2\1\0\5\2\3\0\1\2\3\0\2\2\15\0\13\2\2\0\1\2\21\0\1\2\12\0\6\2\5\0\22"+ - "\2\3\0\10\2\1\0\11\2\1\0\1\2\2\0\7\2\11\0\1\2\1\0\2\2\14\0\12\10\7\0\2\2\1"+ - "\0\1\2\2\0\2\2\1\0\1\2\2\0\1\2\6\0\4\2\1\0\7\2\1\0\3\2\1\0\1\2\1\0\1\2\2\0"+ - "\2\2\1\0\4\2\1\0\2\2\11\0\1\2\2\0\5\2\1\0\1\2\11\0\12\10\2\0\14\2\1\0\24\2"+ - "\13\0\5\2\3\0\6\2\4\0\4\2\3\0\1\2\3\0\2\2\7\0\3\2\4\0\15\2\14\0\1\2\1\0\6"+ - "\2\1\0\1\2\5\0\1\2\2\0\13\2\1\0\15\2\1\0\4\2\2\0\7\2\1\0\1\2\1\0\4\2\2\0\1"+ - "\2\1\0\4\2\2\0\7\2\1\0\1\2\1\0\4\2\2\0\16\2\2\0\6\2\2\0\15\2\2\0\1\2\1\1\17"+ - "\2\1\0\10\2\7\0\15\2\1\0\6\2\23\0\1\2\4\0\1\2\3\0\11\2\1\0\1\2\5\0\17\2\1"+ - "\0\16\2\2\0\14\2\13\0\1\2\15\0\7\2\7\0\16\2\15\0\2\2\12\10\3\0\3\2\11\0\4"+ - "\2\1\0\4\2\3\0\2\2\11\0\10\2\1\0\1\2\1\0\1\2\1\0\1\2\1\0\6\2\1\0\7\2\1\0\1"+ - "\2\3\0\3\2\1\0\7\2\3\0\4\2\2\0\6\2\4\0\13\1\15\0\2\1\5\0\1\1\17\0\1\1\1\0"+ - "\1\2\15\0\1\2\2\0\1\2\4\0\1\2\2\0\12\2\1\0\1\2\3\0\5\2\6\0\1\2\1\0\1\2\1\0"+ - "\1\2\1\0\4\2\1\0\13\2\2\0\4\2\5\0\5\2\4\0\1\2\4\0\2\2\13\0\5\2\6\0\4\2\3\0"+ - "\2\2\14\0\10\2\7\0\10\2\1\0\7\2\1\0\1\1\4\0\2\2\12\0\5\2\5\0\2\2\3\0\7\2\6"+ - "\0\3\2\12\10\2\2\13\0\11\2\2\0\27\2\2\0\7\2\1\0\3\2\1\0\4\2\1\0\4\2\2\0\6"+ - "\2\3\0\1\2\1\0\1\2\2\0\5\2\1\0\12\2\12\10\5\2\1\0\3\2\1\0\10\2\4\0\7\2\3\0"+ - "\1\2\3\0\2\2\1\0\1\2\3\0\2\2\2\0\5\2\2\0\1\2\1\0\1\2\30\0\3\2\3\0\6\2\2\0"+ - "\6\2\2\0\6\2\11\0\7\2\4\0\5\2\3\0\5\2\5\0\1\2\1\0\10\2\1\0\5\2\1\0\1\2\1\0"+ - "\2\2\1\0\2\2\1\0\12\2\6\0\12\2\2\0\6\2\2\0\6\2\2\0\6\2\2\0\3\2\3\0\14\2\1"+ - "\0\16\2\1\0\2\2\1\0\2\2\1\0\10\2\6\0\4\2\4\0\16\2\2\0\1\2\1\0\14\2\1\0\2\2"+ - "\3\0\1\2\2\0\4\2\1\0\2\2\12\0\10\2\6\0\6\2\1\0\3\2\1\0\12\2\3\0\1\2\12\0\4"+ - "\2\13\0\12\10\1\2\1\0\1\2\3\0\7\2\1\0\1\2\1\0\4\2\1\0\17\2\1\0\2\2\14\0\3"+ - "\2\4\0\2\2\1\0\1\2\20\0\4\2\10\0\1\2\13\0\10\2\5\0\3\2\2\0\1\2\2\0\2\2\2\0"+ - "\4\2\1\0\14\2\1\0\1\2\1\0\7\2\1\0\21\2\1\0\4\2\2\0\10\2\1\0\7\2\1\0\14\2\1"+ - "\0\4\2\1\0\5\2\1\0\1\2\3\0\14\2\2\0\13\2\1\0\10\2\2\0\22\10\1\0\2\2\1\0\1"+ - "\2\2\0\1\2\1\0\12\2\1\0\4\2\1\0\1\2\1\0\1\2\6\0\1\2\4\0\1\2\1\0\1\2\1\0\1"+ - "\2\1\0\3\2\1\0\2\2\1\0\1\2\2\0\1\2\1\0\1\2\1\0\1\2\1\0\1\2\1\0\1\2\1\0\2\2"+ - "\1\0\1\2\2\0\4\2\1\0\7\2\1\0\4\2\1\0\4\2\1\0\1\2\1\0\12\2\1\0\5\2\1\0\3\2"+ - "\1\0\5\2\1\0\5\2"); + "\11\0\5\2\22\0\1\2\1\7\1\14\4\0\1\15\5\0\1\10\2\7\12\12\1\6\2\0\1\16\3\0\32"+ + "\5\1\0\1\6\2\0\1\4\13\5\3\0\1\13\6\0\1\1\12\0\1\1\11\0\1\3\12\0\1\3\4\0\1"+ + "\3\5\0\27\3\1\0\12\3\4\0\14\3\16\0\5\3\7\0\1\3\1\0\1\3\1\0\5\3\1\0\2\3\2\0"+ + "\4\3\1\0\1\3\6\0\1\3\1\0\3\3\1\0\1\3\1\0\4\3\1\0\23\3\1\0\13\3\10\0\6\3\1"+ + "\0\26\3\2\0\1\3\6\0\10\3\10\0\13\3\5\0\3\3\15\0\12\11\4\0\6\3\1\0\1\3\17\0"+ + "\2\3\7\0\2\3\12\11\3\3\2\0\2\3\1\0\16\3\15\0\11\3\13\0\1\3\16\0\12\11\6\3"+ + "\4\0\2\3\4\0\1\3\5\0\6\3\4\0\1\3\11\0\1\3\3\0\1\3\7\0\11\3\7\0\5\3\17\0\26"+ + "\3\3\0\1\3\2\0\1\3\7\0\12\3\4\0\12\11\1\3\4\0\10\3\2\0\2\3\2\0\26\3\1\0\7"+ + "\3\1\0\1\3\3\0\4\3\3\0\1\3\20\0\1\3\15\0\2\3\1\0\1\3\5\0\6\3\4\0\2\3\1\0\2"+ + "\3\1\0\2\3\1\0\2\3\17\0\4\3\1\0\1\3\7\0\12\11\2\0\3\3\20\0\11\3\1\0\2\3\1"+ + "\0\2\3\1\0\5\3\3\0\1\3\2\0\1\3\30\0\1\3\13\0\10\3\2\0\1\3\3\0\1\3\1\0\6\3"+ + "\3\0\3\3\1\0\4\3\3\0\2\3\1\0\1\3\1\0\2\3\3\0\2\3\3\0\3\3\3\0\14\3\13\0\10"+ + "\3\1\0\2\3\10\0\3\3\5\0\4\3\1\0\5\3\3\0\1\3\3\0\2\3\15\0\13\3\2\0\1\3\21\0"+ + "\1\3\12\0\6\3\5\0\22\3\3\0\10\3\1\0\11\3\1\0\1\3\2\0\7\3\11\0\1\3\1\0\2\3"+ + "\14\0\12\11\7\0\2\3\1\0\1\3\2\0\2\3\1\0\1\3\2\0\1\3\6\0\4\3\1\0\7\3\1\0\3"+ + "\3\1\0\1\3\1\0\1\3\2\0\2\3\1\0\4\3\1\0\2\3\11\0\1\3\2\0\5\3\1\0\1\3\11\0\12"+ + "\11\2\0\14\3\1\0\24\3\13\0\5\3\3\0\6\3\4\0\4\3\3\0\1\3\3\0\2\3\7\0\3\3\4\0"+ + "\15\3\14\0\1\3\1\0\6\3\1\0\1\3\5\0\1\3\2\0\13\3\1\0\15\3\1\0\4\3\2\0\7\3\1"+ + "\0\1\3\1\0\4\3\2\0\1\3\1\0\4\3\2\0\7\3\1\0\1\3\1\0\4\3\2\0\16\3\2\0\6\3\2"+ + "\0\15\3\2\0\1\3\1\1\17\3\1\0\10\3\7\0\15\3\1\0\6\3\23\0\1\3\4\0\1\3\3\0\11"+ + "\3\1\0\1\3\5\0\17\3\1\0\16\3\2\0\14\3\13\0\1\3\15\0\7\3\7\0\16\3\15\0\2\3"+ + "\12\11\3\0\3\3\11\0\4\3\1\0\4\3\3\0\2\3\11\0\10\3\1\0\1\3\1\0\1\3\1\0\1\3"+ + "\1\0\6\3\1\0\7\3\1\0\1\3\3\0\3\3\1\0\7\3\3\0\4\3\2\0\6\3\4\0\13\1\15\0\2\1"+ + "\5\0\1\1\17\0\1\1\1\0\1\3\15\0\1\3\2\0\1\3\4\0\1\3\2\0\12\3\1\0\1\3\3\0\5"+ + "\3\6\0\1\3\1\0\1\3\1\0\1\3\1\0\4\3\1\0\13\3\2\0\4\3\5\0\5\3\4\0\1\3\4\0\2"+ + "\3\13\0\5\3\6\0\4\3\3\0\2\3\14\0\10\3\7\0\10\3\1\0\7\3\1\0\1\1\4\0\2\3\12"+ + "\0\5\3\5\0\2\3\3\0\7\3\6\0\3\3\12\11\2\3\13\0\11\3\2\0\27\3\2\0\7\3\1\0\3"+ + "\3\1\0\4\3\1\0\4\3\2\0\6\3\3\0\1\3\1\0\1\3\2\0\5\3\1\0\12\3\12\11\5\3\1\0"+ + "\3\3\1\0\10\3\4\0\7\3\3\0\1\3\3\0\2\3\1\0\1\3\3\0\2\3\2\0\5\3\2\0\1\3\1\0"+ + "\1\3\30\0\3\3\3\0\6\3\2\0\6\3\2\0\6\3\11\0\7\3\4\0\5\3\3\0\5\3\5\0\1\3\1\0"+ + "\10\3\1\0\5\3\1\0\1\3\1\0\2\3\1\0\2\3\1\0\12\3\6\0\12\3\2\0\6\3\2\0\6\3\2"+ + "\0\6\3\2\0\3\3\3\0\14\3\1\0\16\3\1\0\2\3\1\0\2\3\1\0\10\3\6\0\4\3\4\0\16\3"+ + "\2\0\1\3\1\0\14\3\1\0\2\3\3\0\1\3\2\0\4\3\1\0\2\3\12\0\10\3\6\0\6\3\1\0\3"+ + "\3\1\0\12\3\3\0\1\3\12\0\4\3\13\0\12\11\1\3\1\0\1\3\3\0\7\3\1\0\1\3\1\0\4"+ + "\3\1\0\17\3\1\0\2\3\14\0\3\3\4\0\2\3\1\0\1\3\20\0\4\3\10\0\1\3\13\0\10\3\5"+ + "\0\3\3\2\0\1\3\2\0\2\3\2\0\4\3\1\0\14\3\1\0\1\3\1\0\7\3\1\0\21\3\1\0\4\3\2"+ + "\0\10\3\1\0\7\3\1\0\14\3\1\0\4\3\1\0\5\3\1\0\1\3\3\0\14\3\2\0\13\3\1\0\10"+ + "\3\2\0\22\11\1\0\2\3\1\0\1\3\2\0\1\3\1\0\12\3\1\0\4\3\1\0\1\3\1\0\1\3\6\0"+ + "\1\3\4\0\1\3\1\0\1\3\1\0\1\3\1\0\3\3\1\0\2\3\1\0\1\3\2\0\1\3\1\0\1\3\1\0\1"+ + "\3\1\0\1\3\1\0\1\3\1\0\2\3\1\0\1\3\2\0\4\3\1\0\7\3\1\0\4\3\1\0\4\3\1\0\1\3"+ + "\1\0\12\3\1\0\5\3\1\0\3\3\1\0\5\3\1\0\5\3"); /** * Translates DFA states to action switch labels. @@ -157,11 +160,12 @@ public class _CommandLineLexer implements FlexLexer { private static final int [] ZZ_ACTION = zzUnpackAction(); private static final String ZZ_ACTION_PACKED_0 = - "\1\0\1\1\1\2\1\3\1\4\1\1\1\5\1\6"+ - "\1\7\1\0\1\10"; + "\1\0\1\1\1\2\1\3\1\4\1\1\1\5\2\1"+ + "\1\6\1\7\7\0\1\10\1\11\1\12\1\13\1\14"+ + "\1\15\1\16"; private static int [] zzUnpackAction() { - int [] result = new int[11]; + int [] result = new int[25]; int offset = 0; offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); return result; @@ -186,11 +190,13 @@ public class _CommandLineLexer implements FlexLexer { private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); private static final String ZZ_ROWMAP_PACKED_0 = - "\0\0\0\14\0\30\0\44\0\60\0\74\0\110\0\14"+ - "\0\14\0\124\0\140"; + "\0\0\0\17\0\36\0\55\0\74\0\113\0\132\0\151"+ + "\0\170\0\17\0\17\0\207\0\226\0\245\0\264\0\303"+ + "\0\322\0\341\0\360\0\17\0\17\0\17\0\17\0\17"+ + "\0\17"; private static int [] zzUnpackRowMap() { - int [] result = new int[11]; + int [] result = new int[25]; int offset = 0; offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); return result; @@ -213,15 +219,23 @@ public class _CommandLineLexer implements FlexLexer { private static final int [] ZZ_TRANS = zzUnpackTrans(); private static final String ZZ_TRANS_PACKED_0 = - "\1\2\1\3\1\4\1\2\1\4\1\2\1\5\1\6"+ - "\2\7\1\5\1\10\15\0\1\3\15\0\5\4\1\0"+ - "\1\4\5\0\5\5\1\0\1\5\4\0\1\11\1\0"+ - "\1\11\2\0\1\12\7\0\5\7\1\0\1\7\4\0"+ - "\1\13\1\0\1\13\12\0\2\13\2\0\1\13\1\0"+ - "\1\13\2\0"; + "\1\2\2\3\1\4\1\2\1\4\1\2\1\5\1\6"+ + "\2\7\1\5\1\10\1\11\1\12\20\0\2\3\20\0"+ + "\5\4\1\0\1\4\10\0\5\5\1\0\1\5\7\0"+ + "\1\13\1\0\1\13\2\0\1\14\12\0\5\7\1\0"+ + "\1\7\7\0\1\15\1\0\1\15\1\0\1\16\1\0"+ + "\2\17\1\16\6\0\1\20\1\0\1\20\1\0\1\21"+ + "\1\0\2\22\1\21\6\0\1\23\1\0\1\23\13\0"+ + "\1\15\1\0\5\15\1\0\1\15\1\0\1\24\4\0"+ + "\1\16\1\0\5\16\1\0\1\16\1\0\1\25\4\0"+ + "\1\17\1\0\5\17\1\0\1\17\1\0\1\26\4\0"+ + "\1\20\1\0\5\20\1\0\1\20\2\0\1\27\3\0"+ + "\1\21\1\0\5\21\1\0\1\21\2\0\1\30\3\0"+ + "\1\22\1\0\5\22\1\0\1\22\2\0\1\31\5\0"+ + "\2\23\2\0\1\23\1\0\1\23\4\0"; private static int [] zzUnpackTrans() { - int [] result = new int[108]; + int [] result = new int[255]; int offset = 0; offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); return result; @@ -259,10 +273,10 @@ public class _CommandLineLexer implements FlexLexer { private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); private static final String ZZ_ATTRIBUTE_PACKED_0 = - "\1\0\1\11\5\1\2\11\1\0\1\1"; + "\1\0\1\11\7\1\2\11\7\0\1\1\6\11"; private static int [] zzUnpackAttribute() { - int [] result = new int[11]; + int [] result = new int[25]; int offset = 0; offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); return result; @@ -570,35 +584,59 @@ public class _CommandLineLexer implements FlexLexer { case 1: { return BAD_CHARACTER; } - case 9: break; + case 15: break; case 2: { return WHITE_SPACE; } - case 10: break; + case 16: break; case 3: { return LITERAL_STARTS_FROM_LETTER; } - case 11: break; + case 17: break; case 4: { return LITERAL_STARTS_FROM_SYMBOL; } - case 12: break; + case 18: break; case 5: { return LITERAL_STARTS_FROM_DIGIT; } - case 13: break; + case 19: break; case 6: { return EQ; } - case 14: break; + case 20: break; case 7: { return SHORT_OPTION_NAME_TOKEN; } - case 15: break; + case 21: break; case 8: { return LONG_OPTION_NAME_TOKEN; } - case 16: break; + case 22: break; + case 9: + { return SPACED_LITERAL_STARTS_FROM_LETTER; + } + case 23: break; + case 10: + { return SPACED_LITERAL_STARTS_FROM_SYMBOL; + } + case 24: break; + case 11: + { return SPACED_LITERAL_STARTS_FROM_DIGIT; + } + case 25: break; + case 12: + { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER; + } + case 26: break; + case 13: + { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL; + } + case 27: break; + case 14: + { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT; + } + case 28: break; default: zzScanError(ZZ_NO_MATCH); } diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineArgument.java b/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineArgument.java index 02d91dff0f79..64f043d0bb0f 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineArgument.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineArgument.java @@ -20,6 +20,24 @@ public interface CommandLineArgument extends CommandLinePart { @Nullable PsiElement getLiteralStartsFromSymbol(); + @Nullable + PsiElement getSingleQSpacedLiteralStartsFromDigit(); + + @Nullable + PsiElement getSingleQSpacedLiteralStartsFromLetter(); + + @Nullable + PsiElement getSingleQSpacedLiteralStartsFromSymbol(); + + @Nullable + PsiElement getSpacedLiteralStartsFromDigit(); + + @Nullable + PsiElement getSpacedLiteralStartsFromLetter(); + + @Nullable + PsiElement getSpacedLiteralStartsFromSymbol(); + @Nullable Option findOptionForOptionArgument(); @@ -29,4 +47,7 @@ public interface CommandLineArgument extends CommandLinePart { @Nullable Help findBestHelp(); + @NotNull + String getValueNoQuotes(); + } diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineOption.java b/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineOption.java index 467fb4a9a3f8..b5e691ce3584 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineOption.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/psi/CommandLineOption.java @@ -24,4 +24,7 @@ public interface CommandLineOption extends CommandLinePart { @Nullable Option findRealOption(); + @Nullable + CommandLineArgument findArgument(); + } diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineArgumentImpl.java b/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineArgumentImpl.java index 3bdc0f09430d..43a66de1ee61 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineArgumentImpl.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineArgumentImpl.java @@ -47,6 +47,42 @@ public class CommandLineArgumentImpl extends CommandLineElement implements Comma return findChildByType(LITERAL_STARTS_FROM_SYMBOL); } + @Override + @Nullable + public PsiElement getSingleQSpacedLiteralStartsFromDigit() { + return findChildByType(SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT); + } + + @Override + @Nullable + public PsiElement getSingleQSpacedLiteralStartsFromLetter() { + return findChildByType(SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER); + } + + @Override + @Nullable + public PsiElement getSingleQSpacedLiteralStartsFromSymbol() { + return findChildByType(SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL); + } + + @Override + @Nullable + public PsiElement getSpacedLiteralStartsFromDigit() { + return findChildByType(SPACED_LITERAL_STARTS_FROM_DIGIT); + } + + @Override + @Nullable + public PsiElement getSpacedLiteralStartsFromLetter() { + return findChildByType(SPACED_LITERAL_STARTS_FROM_LETTER); + } + + @Override + @Nullable + public PsiElement getSpacedLiteralStartsFromSymbol() { + return findChildByType(SPACED_LITERAL_STARTS_FROM_SYMBOL); + } + @Nullable public Option findOptionForOptionArgument() { return CommandLinePsiImplUtils.findOptionForOptionArgument(this); @@ -62,4 +98,9 @@ public class CommandLineArgumentImpl extends CommandLineElement implements Comma return CommandLinePsiImplUtils.findBestHelp(this); } + @NotNull + public String getValueNoQuotes() { + return CommandLinePsiImplUtils.getValueNoQuotes(this); + } + } diff --git a/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineOptionImpl.java b/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineOptionImpl.java index dbfb17896c45..53cc7eeb2f1f 100644 --- a/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineOptionImpl.java +++ b/python/gen/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLineOptionImpl.java @@ -54,4 +54,9 @@ public class CommandLineOptionImpl extends CommandLineElement implements Command return CommandLinePsiImplUtils.findRealOption(this); } + @Nullable + public CommandLineArgument findArgument() { + return CommandLinePsiImplUtils.findArgument(this); + } + } diff --git a/python/helpers/pycharm/_jb_runner_tools.py b/python/helpers/pycharm/_jb_runner_tools.py index 8ac67c1699ca..f05f4b314575 100644 --- a/python/helpers/pycharm/_jb_runner_tools.py +++ b/python/helpers/pycharm/_jb_runner_tools.py @@ -207,20 +207,19 @@ class NewTeamcityServiceMessages(_old_service_messages): # Blocks are used for 2 cases now: # 1) Unittest subtests # 2) setup/teardown (does not work, see https://github.com/JetBrains/teamcity-messages/issues/114) - def blockOpened(self, name, flowId=None): - self.testStarted(".".join(TREE_MANAGER.current_branch + [self._fix_setup_teardown_name(name)])) + # def blockOpened(self, name, flowId=None): + # self.testStarted(".".join(TREE_MANAGER.current_branch + [self._fix_setup_teardown_name(name)])) def blockClosed(self, name, flowId=None): - test_name = ".".join(TREE_MANAGER.current_branch + [self._fix_setup_teardown_name(name)]) - if self._latest_subtest_result: - self.testFinished(test_name) - else: + test_name = ".".join(TREE_MANAGER.current_branch) + if self._latest_subtest_result == "Failure": self.testFailed(test_name) + self.testFinished(test_name) self._latest_subtest_result = None def subTestBlockOpened(self, name, subTestResult, flowId=None): - self.testStarted(".".join(TREE_MANAGER.current_branch + [self._fix_setup_teardown_name(name)])) + self.testStarted(".".join(TREE_MANAGER.current_branch + [name])) self._latest_subtest_result = subTestResult def testStarted(self, testName, captureStandardOutput=None, flowId=None, is_suite=False): diff --git a/python/helpers/pydev/_pydevd_frame_eval/pydevd_modify_bytecode.py b/python/helpers/pydev/_pydevd_frame_eval/pydevd_modify_bytecode.py index 81ca48568236..aa46cb7fdc6f 100644 --- a/python/helpers/pydev/_pydevd_frame_eval/pydevd_modify_bytecode.py +++ b/python/helpers/pydev/_pydevd_frame_eval/pydevd_modify_bytecode.py @@ -1,6 +1,7 @@ import dis +import traceback +from opcode import opmap, EXTENDED_ARG from types import CodeType -from opcode import opmap MAX_BYTE = 255 @@ -17,69 +18,107 @@ def _add_attr_values_from_insert_to_original(original_code, insert_code, insert_ :param insert_code_obj: bytes sequence of inserted code, which should be modified too :param attribute_name: name of attribute to modify ('co_names', 'co_consts' or 'co_varnames') :param op_list: sequence of bytecodes whose arguments should be changed - :return: modified bytes sequence of the code to insert and new values of the attribute `attribute_name` for original code + :return: modified bytes sequence of the code to insert and new values of the attribute `attribute_name` for + original code """ orig_value = getattr(original_code, attribute_name) insert_value = getattr(insert_code, attribute_name) orig_names_len = len(orig_value) code_with_new_values = list(insert_code_obj) - for offset, op, arg in dis._unpack_opargs(insert_code_obj): + offset = 0 + while offset < len(code_with_new_values): + op = code_with_new_values[offset] if op in op_list: - if code_with_new_values[offset + 1] + orig_names_len > MAX_BYTE: - raise ValueError("Bad number of arguments") - code_with_new_values[offset + 1] += orig_names_len + new_val = code_with_new_values[offset + 1] + orig_names_len + if new_val > MAX_BYTE: + code_with_new_values[offset + 1] = new_val & MAX_BYTE + code_with_new_values = code_with_new_values[:offset] + [EXTENDED_ARG, new_val >> 8] + \ + code_with_new_values[offset:] + offset += 2 + else: + code_with_new_values[offset + 1] = new_val + offset += 2 new_values = orig_value + insert_value return bytes(code_with_new_values), new_values -def _modify_new_lines(code_to_modify, code_insert, offset_of_inserted_code): +def _modify_new_lines(code_to_modify, all_inserted_code): """ Update new lines in order to hide inserted code inside the original code :param code_to_modify: code to modify - :param code_insert: code to insert - :param offset_of_inserted_code: the offset of the inserted code + :param all_inserted_code: list of tuples (offset, list of code instructions) with all inserted pieces of code :return: bytes sequence of code with updated lines offsets """ new_list = list(code_to_modify.co_lnotab) - abs_offset = 0 - for i in range(0, len(new_list), 2): + abs_offset = prev_abs_offset = 0 + i = 0 + while i < len(new_list): + prev_abs_offset = abs_offset abs_offset += new_list[i] - if abs_offset == offset_of_inserted_code and (i + 2) < len(new_list): - if new_list[i + 2] + len(code_insert) > MAX_BYTE: - raise ValueError("Bad number of arguments") - new_list[i + 2] += len(code_insert) + for (inserted_offset, inserted_code) in all_inserted_code: + if prev_abs_offset <= inserted_offset < abs_offset: + size_of_inserted = len(inserted_code) + new_list[i] += size_of_inserted + abs_offset += size_of_inserted + if new_list[i] > MAX_BYTE: + new_list[i] = new_list[i] - MAX_BYTE + new_list = new_list[:i] + [MAX_BYTE, 0] + new_list[i:] + i += 2 return bytes(new_list) -def _update_label_offsets(code_obj, offset_of_inserted_code, size_of_inserted_code): +def _update_label_offsets(code_obj, breakpoint_offset, breakpoint_code_list): """ Update labels for the relative and absolute jump targets :param code_obj: code to modify - :param offset_of_inserted_code: offset for the inserted code - :param offset_of_inserted_code: size of the inserted code - :return: bytes sequence with modified labels + :param breakpoint_offset: offset for the inserted code + :param breakpoint_code_list: size of the inserted code + :return: bytes sequence with modified labels; list of tuples (resulting offset, list of code instructions) with + information about all inserted pieces of code """ - offsets_for_modification = [] - for offset, op, arg in dis._unpack_opargs(code_obj): - if arg is not None: - if op in dis.hasjrel: - # has relative jump target - label = offset + 2 + arg - if offset < offset_of_inserted_code < label: - # change labels for relative jump targets if code was inserted inside - offsets_for_modification.append(offset) - elif op in dis.hasjabs: - # change label for absolute jump if code was inserted before it - if offset_of_inserted_code <= arg: - offsets_for_modification.append(offset) + inserted_code = list() + # the list with all inserted pieces of code + inserted_code.append((breakpoint_offset, breakpoint_code_list)) code_list = list(code_obj) - for i in range(0, len(code_obj), 2): - op = code_list[i] - if i in offsets_for_modification and op >= dis.HAVE_ARGUMENT: - if code_list[i + 1] + size_of_inserted_code > MAX_BYTE: - raise ValueError("Bad jump argument") - code_list[i + 1] += size_of_inserted_code - return bytes(code_list) + j = 0 + + while j < len(inserted_code): + current_offset, current_code_list = inserted_code[j] + offsets_for_modification = [] + + for offset, op, arg in dis._unpack_opargs(code_list): + if arg is not None: + if op in dis.hasjrel: + # has relative jump target + label = offset + 2 + arg + if offset < current_offset < label: + # change labels for relative jump targets if code was inserted inside + offsets_for_modification.append(offset) + elif op in dis.hasjabs: + # change label for absolute jump if code was inserted before it + if current_offset <= arg: + offsets_for_modification.append(offset) + for i in range(0, len(code_list), 2): + op = code_list[i] + if i in offsets_for_modification and op >= dis.HAVE_ARGUMENT: + new_arg = code_list[i + 1] + len(current_code_list) + if new_arg <= MAX_BYTE: + code_list[i + 1] = new_arg + else: + # if new argument > 255 we need to insert the new operator EXTENDED_ARG + extended_arg_code = [EXTENDED_ARG, new_arg >> 8] + code_list[i + 1] = new_arg & MAX_BYTE + inserted_code.append((i, extended_arg_code)) + + code_list = code_list[:current_offset] + current_code_list + code_list[current_offset:] + + for k in range(len(inserted_code)): + offset, inserted_code_list = inserted_code[k] + if current_offset < offset: + inserted_code[k] = (offset + len(current_code_list), inserted_code_list) + j += 1 + + return bytes(code_list), inserted_code def _return_none_fun(): @@ -94,7 +133,7 @@ def insert_code(code_to_modify, code_to_insert, before_line): :param code_to_modify: Code to modify :param code_to_insert: Code to insert :param before_line: Number of line for code insertion - :return: modified code + :return: boolean flag whether insertion was successful, modified code """ linestarts = dict(dis.findlinestarts(code_to_modify)) if before_line not in linestarts.values(): @@ -108,16 +147,19 @@ def insert_code(code_to_modify, code_to_insert, before_line): code_to_insert_obj = code_to_insert.co_code[:-return_none_size] try: code_to_insert_obj, new_names = \ - _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_names', dis.hasname) + _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_names', + dis.hasname) code_to_insert_obj, new_consts = \ - _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_consts', [opmap['LOAD_CONST']]) + _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_consts', + [opmap['LOAD_CONST']]) code_to_insert_obj, new_vars = \ - _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_varnames', dis.haslocal) - modified_code = _update_label_offsets(code_to_modify.co_code, offset, len(code_to_insert_obj)) - new_bytes = modified_code[:offset] + code_to_insert_obj + modified_code[offset:] + _add_attr_values_from_insert_to_original(code_to_modify, code_to_insert, code_to_insert_obj, 'co_varnames', + dis.haslocal) + new_bytes, all_inserted_code = _update_label_offsets(code_to_modify.co_code, offset, list(code_to_insert_obj)) - new_lnotab = _modify_new_lines(code_to_modify, code_to_insert_obj, offset) + new_lnotab = _modify_new_lines(code_to_modify, all_inserted_code) except ValueError: + traceback.print_exc() return False, code_to_modify new_code = CodeType( diff --git a/python/helpers/pydev/tests_pydevd_python/_many_names_example.py b/python/helpers/pydev/tests_pydevd_python/_many_names_example.py new file mode 100644 index 000000000000..6221725a3b60 --- /dev/null +++ b/python/helpers/pydev/tests_pydevd_python/_many_names_example.py @@ -0,0 +1,268 @@ + + +def foo(): + a0 = 1 + a1 = 1 + a2 = 1 + a3 = 1 + a4 = 1 + a5 = 1 + a6 = 1 + a7 = 1 + a8 = 1 + a9 = 1 + a10 = 1 + a11 = 1 + a12 = 1 + a13 = 1 + a14 = 1 + a15 = 1 + a16 = 1 + a17 = 1 + a18 = 1 + a19 = 1 + a20 = 1 + a21 = 1 + a22 = 1 + a23 = 1 + a24 = 1 + a25 = 1 + a26 = 1 + a27 = 1 + a28 = 1 + a29 = 1 + a30 = 1 + a31 = 1 + a32 = 1 + a33 = 1 + a34 = 1 + a35 = 1 + a36 = 1 + a37 = 1 + a38 = 1 + a39 = 1 + a40 = 1 + a41 = 1 + a42 = 1 + a43 = 1 + a44 = 1 + a45 = 1 + a46 = 1 + a47 = 1 + a48 = 1 + a49 = 1 + a50 = 1 + a51 = 1 + a52 = 1 + a53 = 1 + a54 = 1 + a55 = 1 + a56 = 1 + a57 = 1 + a58 = 1 + a59 = 1 + a60 = 1 + a61 = 1 + a62 = 1 + a63 = 1 + a64 = 1 + a65 = 1 + a66 = 1 + a67 = 1 + a68 = 1 + a69 = 1 + a70 = 1 + a71 = 1 + a72 = 1 + a73 = 1 + a74 = 1 + a75 = 1 + a76 = 1 + a77 = 1 + a78 = 1 + a79 = 1 + a80 = 1 + a81 = 1 + a82 = 1 + a83 = 1 + a84 = 1 + a85 = 1 + a86 = 1 + a87 = 1 + a88 = 1 + a89 = 1 + a90 = 1 + a91 = 1 + a92 = 1 + a93 = 1 + a94 = 1 + a95 = 1 + a96 = 1 + a97 = 1 + a98 = 1 + a99 = 1 + a100 = 1 + a101 = 1 + a102 = 1 + a103 = 1 + a104 = 1 + a105 = 1 + a106 = 1 + a107 = 1 + a108 = 1 + a109 = 1 + a110 = 1 + a111 = 1 + a112 = 1 + a113 = 1 + a114 = 1 + a115 = 1 + a116 = 1 + a117 = 1 + a118 = 1 + a119 = 1 + a120 = 1 + a121 = 1 + a122 = 1 + a123 = 1 + a124 = 1 + a125 = 1 + a126 = 1 + a127 = 1 + a128 = 1 + a129 = 1 + a130 = 1 + a131 = 1 + a132 = 1 + a133 = 1 + a134 = 1 + a135 = 1 + a136 = 1 + a137 = 1 + a138 = 1 + a139 = 1 + a140 = 1 + a141 = 1 + a142 = 1 + a143 = 1 + a144 = 1 + a145 = 1 + a146 = 1 + a147 = 1 + a148 = 1 + a149 = 1 + a150 = 1 + a151 = 1 + a152 = 1 + a153 = 1 + a154 = 1 + a155 = 1 + a156 = 1 + a157 = 1 + a158 = 1 + a159 = 1 + a160 = 1 + a161 = 1 + a162 = 1 + a163 = 1 + a164 = 1 + a165 = 1 + a166 = 1 + a167 = 1 + a168 = 1 + a169 = 1 + a170 = 1 + a171 = 1 + a172 = 1 + a173 = 1 + a174 = 1 + a175 = 1 + a176 = 1 + a177 = 1 + a178 = 1 + a179 = 1 + a180 = 1 + a181 = 1 + a182 = 1 + a183 = 1 + a184 = 1 + a185 = 1 + a186 = 1 + a187 = 1 + a188 = 1 + a189 = 1 + a190 = 1 + a191 = 1 + a192 = 1 + a193 = 1 + a194 = 1 + a195 = 1 + a196 = 1 + a197 = 1 + a198 = 1 + a199 = 1 + a200 = 1 + a201 = 1 + a202 = 1 + a203 = 1 + a204 = 1 + a205 = 1 + a206 = 1 + a207 = 1 + a208 = 1 + a209 = 1 + a210 = 1 + a211 = 1 + a212 = 1 + a213 = 1 + a214 = 1 + a215 = 1 + a216 = 1 + a217 = 1 + a218 = 1 + a219 = 1 + a220 = 1 + a221 = 1 + a222 = 1 + a223 = 1 + a224 = 1 + a225 = 1 + a226 = 1 + a227 = 1 + a228 = 1 + a229 = 1 + a230 = 1 + a231 = 1 + a232 = 1 + a233 = 1 + a234 = 1 + a235 = 1 + a236 = 1 + a237 = 1 + a238 = 1 + a239 = 1 + a240 = 1 + a241 = 1 + a242 = 1 + a243 = 1 + a244 = 1 + a245 = 1 + a246 = 1 + a247 = 1 + a248 = 1 + a249 = 1 + a250 = 1 + a251 = 1 + a252 = 1 + a253 = 1 + a254 = 1 + a255 = 1 + a256 = 1 + a257 = 1 + a258 = 1 + a259 = 1 + b = a1 + a2 + a260 = 1 + a261 = 1 + return b + diff --git a/python/helpers/pydev/tests_pydevd_python/test_bytecode_modification.py b/python/helpers/pydev/tests_pydevd_python/test_bytecode_modification.py index 1583dfc14cbc..cd928bbfe8f8 100644 --- a/python/helpers/pydev/tests_pydevd_python/test_bytecode_modification.py +++ b/python/helpers/pydev/tests_pydevd_python/test_bytecode_modification.py @@ -114,3 +114,68 @@ class TestInsertCode(unittest.TestCase): finally: sys.stdout = self.original_stdout + def test_offset_overflow(self): + self.original_stdout = sys.stdout + sys.stdout = StringIO() + + try: + def foo(): + a = 1 # breakpoint + b = 2 + c = 3 + a1 = 1 if a > 1 else 2 + a2 = 1 if a > 1 else 2 + a3 = 1 if a > 1 else 2 + a4 = 1 if a > 1 else 2 + a5 = 1 if a > 1 else 2 + a6 = 1 if a > 1 else 2 + a7 = 1 if a > 1 else 2 + a8 = 1 if a > 1 else 2 + a9 = 1 if a > 1 else 2 + a10 = 1 if a > 1 else 2 + a11 = 1 if a > 1 else 2 + a12 = 1 if a > 1 else 2 + a13 = 1 if a > 1 else 2 + + for i in range(1): + if a > 0: + print("111") + # a = 1 + else: + print("222") + return b + + self.check_insert_to_line(foo, tracing, foo.__code__.co_firstlineno + 2) + + finally: + sys.stdout = self.original_stdout + + def test_long_lines(self): + self.original_stdout = sys.stdout + sys.stdout = StringIO() + + try: + def foo(): + a = 1 + b = 1 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 + c = 1 if b > 1 else 2 if b > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 + d = 1 if c > 1 else 2 if c > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 if a > 1 else 2 if a > 0 else 3 if a > 4 else 23 + e = d + 1 + return e + + self.check_insert_to_line(foo, tracing, foo.__code__.co_firstlineno + 2) + + + finally: + sys.stdout = self.original_stdout + + def test_many_names(self): + self.original_stdout = sys.stdout + sys.stdout = StringIO() + + try: + from tests_pydevd_python._many_names_example import foo + self.check_insert_to_line(foo, tracing, foo.__code__.co_firstlineno + 2) + + finally: + sys.stdout = self.original_stdout \ No newline at end of file diff --git a/python/python-community-configure/python-community-configure.iml b/python/python-community-configure/python-community-configure.iml index 752e44910b9a..bf105b4e982a 100644 --- a/python/python-community-configure/python-community-configure.iml +++ b/python/python-community-configure/python-community-configure.iml @@ -13,5 +13,6 @@ + \ No newline at end of file diff --git a/python/python-community-configure/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java b/python/python-community-configure/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java index b40ea6f27cfb..3447a4f2a691 100644 --- a/python/python-community-configure/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java +++ b/python/python-community-configure/src/com/jetbrains/python/configuration/PyDiffPreviewProvider.java @@ -15,23 +15,20 @@ */ package com.jetbrains.python.configuration; -import com.intellij.openapi.diff.DiffContent; -import com.intellij.openapi.diff.SimpleContent; +import com.intellij.diff.contents.DiffContent; import com.intellij.openapi.diff.impl.settings.DiffPreviewProvider; import com.jetbrains.python.PythonFileType; import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; /** * @author oleg */ public class PyDiffPreviewProvider extends DiffPreviewProvider { + @NotNull @Override public DiffContent[] createContents() { - return new DiffContent[]{createContent(LEFT_TEXT), createContent(CENTER_TEXT), createContent(RIGHT_TEXT)}; - } - - private static SimpleContent createContent(final String text) { - return new SimpleContent(text, PythonFileType.INSTANCE); + return createContent(LEFT_TEXT, CENTER_TEXT, RIGHT_TEXT, PythonFileType.INSTANCE); } @NonNls private static final String LEFT_TEXT = "class MyClass\n" + diff --git a/python/src/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.flex b/python/src/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.flex index 3a67a142e79c..d75b0ad64835 100644 --- a/python/src/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.flex +++ b/python/src/com/jetbrains/commandInterface/commandLine/_CommandLineLexer.flex @@ -1,7 +1,10 @@ package com.jetbrains.commandInterface.commandLine; -import com.intellij.lexer.*; + +import com.intellij.lexer.FlexLexer; import com.intellij.psi.tree.IElementType; -import static com.intellij.psi.TokenType.*; + +import static com.intellij.psi.TokenType.BAD_CHARACTER; +import static com.intellij.psi.TokenType.WHITE_SPACE; import static com.jetbrains.commandInterface.commandLine.CommandLineElementTypes.*; %% @@ -19,25 +22,40 @@ import static com.jetbrains.commandInterface.commandLine.CommandLineElementTypes %type IElementType %unicode +EOL=\R WHITE_SPACE=\s+ -LITERAL_STARTS_FROM_LETTER=[:letter:]([a-zA-Z_0-9]|:|\\|"/"|\.|-)* -LITERAL_STARTS_FROM_DIGIT=[:digit:]([a-zA-Z_0-9]|:|\\|"/"|\.|-)* -LITERAL_STARTS_FROM_SYMBOL=([/\~\.]([a-zA-Z_0-9]|:|\\|"/"|\.|-)*) +SPACE=[ \t\n\x0B\f\r]+ +LITERAL_STARTS_FROM_LETTER=[:letter:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|\!)* +LITERAL_STARTS_FROM_DIGIT=[:digit:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|\!)* +LITERAL_STARTS_FROM_SYMBOL=([/\~\.!]([a-zA-Z_0-9]|:|\\|"/"|\.|-|\!)*) +SPACED_LITERAL_STARTS_FROM_LETTER=\"[:letter:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*\" +SPACED_LITERAL_STARTS_FROM_DIGIT=\"[:digit:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*\" +SPACED_LITERAL_STARTS_FROM_SYMBOL=\"([/\~\.!]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*)\" +SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER='[:letter:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*' +SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT='[:digit:]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*' +SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL='([/\~\.!]([a-zA-Z_0-9]|:|\\|"/"|\.|-|[ \t\n\x0B\f\r]|\!)*)' SHORT_OPTION_NAME_TOKEN=-[:letter:] LONG_OPTION_NAME_TOKEN=--[:letter:](-|[a-zA-Z_0-9])* %% { - {WHITE_SPACE} { return WHITE_SPACE; } + {WHITE_SPACE} { return WHITE_SPACE; } - "=" { return EQ; } + "=" { return EQ; } - {LITERAL_STARTS_FROM_LETTER} { return LITERAL_STARTS_FROM_LETTER; } - {LITERAL_STARTS_FROM_DIGIT} { return LITERAL_STARTS_FROM_DIGIT; } - {LITERAL_STARTS_FROM_SYMBOL} { return LITERAL_STARTS_FROM_SYMBOL; } - {SHORT_OPTION_NAME_TOKEN} { return SHORT_OPTION_NAME_TOKEN; } - {LONG_OPTION_NAME_TOKEN} { return LONG_OPTION_NAME_TOKEN; } + {SPACE} { return SPACE; } + {LITERAL_STARTS_FROM_LETTER} { return LITERAL_STARTS_FROM_LETTER; } + {LITERAL_STARTS_FROM_DIGIT} { return LITERAL_STARTS_FROM_DIGIT; } + {LITERAL_STARTS_FROM_SYMBOL} { return LITERAL_STARTS_FROM_SYMBOL; } + {SPACED_LITERAL_STARTS_FROM_LETTER} { return SPACED_LITERAL_STARTS_FROM_LETTER; } + {SPACED_LITERAL_STARTS_FROM_DIGIT} { return SPACED_LITERAL_STARTS_FROM_DIGIT; } + {SPACED_LITERAL_STARTS_FROM_SYMBOL} { return SPACED_LITERAL_STARTS_FROM_SYMBOL; } + {SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER} { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER; } + {SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT} { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT; } + {SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL} { return SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL; } + {SHORT_OPTION_NAME_TOKEN} { return SHORT_OPTION_NAME_TOKEN; } + {LONG_OPTION_NAME_TOKEN} { return LONG_OPTION_NAME_TOKEN; } } diff --git a/python/src/com/jetbrains/commandInterface/commandLine/command_line.bnf b/python/src/com/jetbrains/commandInterface/commandLine/command_line.bnf index 8fff17590842..072f69405e35 100644 --- a/python/src/com/jetbrains/commandInterface/commandLine/command_line.bnf +++ b/python/src/com/jetbrains/commandInterface/commandLine/command_line.bnf @@ -25,9 +25,21 @@ tokens=[ space='regexp:\s+' // WARNING: Comment out or remove this (space) before generating Flex file! It is here only for live preview. EQ = '=' - LITERAL_STARTS_FROM_LETTER='regexp:\p{Alpha}(\w|:|\\|/|\.|-)*' - LITERAL_STARTS_FROM_DIGIT='regexp:\p{Digit}(\w|:|\\|/|\.|-)*' - LITERAL_STARTS_FROM_SYMBOL='regexp:([/\~\.](\w|:|\\|/|\.|-)*)' + LITERAL_STARTS_FROM_LETTER='regexp:\p{Alpha}(\w|:|\\|/|\.|-|\!)*' + LITERAL_STARTS_FROM_DIGIT='regexp:\p{Digit}(\w|:|\\|/|\.|-|\!)*' + LITERAL_STARTS_FROM_SYMBOL='regexp:([/\~\.!](\w|:|\\|/|\.|-|\!)*)' + + SPACED_LITERAL_STARTS_FROM_LETTER='regexp:"\p{Alpha}(\w|:|\\|/|\.|-|\s|\!)*"' + SPACED_LITERAL_STARTS_FROM_DIGIT='regexp:"\p{Digit}(\w|:|\\|/|\.|-|\s|\!)*"' + SPACED_LITERAL_STARTS_FROM_SYMBOL='regexp:"([/\~\.!](\w|:|\\|/|\.|-|\s|\!)*)"' + + + SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER="regexp:'\p{Alpha}(\w|:|\\|/|\.|-|\s|\!)*'" + SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT="regexp:'\p{Digit}(\w|:|\\|/|\.|-|\s|\!)*'" + SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL="regexp:'([/\~\.!](\w|:|\\|/|\.|-|\s|\!)*)'" + + + SHORT_OPTION_NAME_TOKEN='regexp:-\p{Alpha}' LONG_OPTION_NAME_TOKEN='regexp:--\p{Alpha}(-|\w)*' ] @@ -37,10 +49,13 @@ root ::= command (argument | option ) * <> command ::= LITERAL_STARTS_FROM_LETTER option ::= (short_option_name <> ? | long_option_name <> ?) { -methods=[ getOptionName isLong findRealOption ] +methods=[ getOptionName isLong findRealOption findArgument ] } private short_option_name ::= SHORT_OPTION_NAME_TOKEN private long_option_name ::= LONG_OPTION_NAME_TOKEN -argument ::= LITERAL_STARTS_FROM_LETTER | LITERAL_STARTS_FROM_DIGIT | LITERAL_STARTS_FROM_SYMBOL { -methods=[ findOptionForOptionArgument findRealArgument findBestHelp ] +argument ::= LITERAL_STARTS_FROM_LETTER | LITERAL_STARTS_FROM_DIGIT | LITERAL_STARTS_FROM_SYMBOL | +SPACED_LITERAL_STARTS_FROM_LETTER | SPACED_LITERAL_STARTS_FROM_DIGIT | SPACED_LITERAL_STARTS_FROM_SYMBOL | +SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER | SINGLE_Q_SPACED_LITERAL_STARTS_FROM_DIGIT | SINGLE_Q_SPACED_LITERAL_STARTS_FROM_SYMBOL +{ +methods=[ findOptionForOptionArgument findRealArgument findBestHelp getValueNoQuotes ] } diff --git a/python/src/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLinePsiImplUtils.java b/python/src/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLinePsiImplUtils.java index 8d7948360a6d..82f3b205977d 100644 --- a/python/src/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLinePsiImplUtils.java +++ b/python/src/com/jetbrains/commandInterface/commandLine/psi/impl/CommandLinePsiImplUtils.java @@ -24,6 +24,7 @@ import com.jetbrains.commandInterface.commandLine.ValidationResult; import com.jetbrains.commandInterface.commandLine.psi.CommandLineArgument; import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile; import com.jetbrains.commandInterface.commandLine.psi.CommandLineOption; +import com.jetbrains.python.psi.PyUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -50,6 +51,18 @@ final class CommandLinePsiImplUtils { return o.getLongOptionNameToken() != null; } + /** + * For options with eq finds argument next to it. + * For options with out of eq just use next psi + * @return null if option does not have eq + */ + @Nullable + static CommandLineArgument findArgument(@NotNull final CommandLineOption option) { + if (option.getText().endsWith("=")) { + return PyUtil.as(option.getNextSibling(), CommandLineArgument.class); + } + return null; + } /** * Finds real option based on psi opton * @@ -65,7 +78,18 @@ final class CommandLinePsiImplUtils { return validationResult.getOption(option); } - + /** + * @return for arg in quotes returns bare value, or simply value otherwise + */ + @NotNull + static String getValueNoQuotes(@NotNull final CommandLineArgument argument) { + final char[] chars = argument.getText().toCharArray(); + final char firstChar = chars[0]; + if (firstChar == chars[chars.length - 1] && firstChar == '"' || firstChar == '\'') { + return argument.getText().substring(1, argument.getTextLength() - 1); + } + return argument.getText(); + } /** * Tries to find appropriate help for argument. It can be argument help for positional argument or option help * for option argument. diff --git a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java index 3e867147cf1b..29225aba8798 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java @@ -71,6 +71,9 @@ public class PyElementGeneratorImpl extends PyElementGenerator { return createDummyFile(langLevel, contents, false); } + /** + * TODO: Use {@link PsiFileFactory} instead? + */ public PsiFile createDummyFile(LanguageLevel langLevel, String contents, boolean physical) { final PsiFileFactory factory = PsiFileFactory.getInstance(myProject); final String name = getDummyFileName(); diff --git a/python/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java index 5eacbe58be07..59de303a73fb 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java @@ -94,16 +94,21 @@ public abstract class PythonSdkFlavor { result.addAll(getPlatformIndependentFlavors()); } - for (PythonFlavorProvider provider : Extensions.getExtensions(PythonFlavorProvider.EP_NAME)) { - PythonSdkFlavor flavor = provider.getFlavor(addPlatformIndependent); - if (flavor != null) { - result.add(flavor); - } - } + result.addAll(getPlatformFlavorsFromExtensions(addPlatformIndependent)); return result; } + public static List getPlatformFlavorsFromExtensions(boolean isInpedendent) { + List result = new ArrayList<>(); + for (PythonFlavorProvider provider : Extensions.getExtensions(PythonFlavorProvider.EP_NAME)) { + PythonSdkFlavor flavor = provider.getFlavor(isInpedendent); + if (flavor != null) { + result.add(flavor); + } + } + return result; + } public static List getPlatformIndependentFlavors() { List result = Lists.newArrayList(); @@ -149,6 +154,12 @@ public abstract class PythonSdkFlavor { return flavor; } } + + for (PythonSdkFlavor flavor: getPlatformFlavorsFromExtensions(true)) { + if (flavor.isValidSdkHome(sdkPath)) { + return flavor; + } + } return null; } diff --git a/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt b/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt index 884554da99f0..4a69aceaeda3 100644 --- a/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt +++ b/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt @@ -15,14 +15,22 @@ */ package com.jetbrains.python.testing.universalTests +import com.intellij.execution.ExecutionException import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiFileFactory import com.intellij.psi.PsiFileSystemItem import com.intellij.psi.PsiManager import com.intellij.psi.util.QualifiedName +import com.jetbrains.commandInterface.commandLine.CommandLineLanguage +import com.jetbrains.commandInterface.commandLine.CommandLinePart +import com.jetbrains.commandInterface.commandLine.psi.CommandLineArgument +import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile +import com.jetbrains.commandInterface.commandLine.psi.CommandLineOption import com.jetbrains.extensions.getQName import com.jetbrains.python.PyNames import com.jetbrains.python.psi.PyFile @@ -30,6 +38,7 @@ import com.jetbrains.python.psi.PyQualifiedNameOwner import com.jetbrains.python.psi.PyUtil import com.jetbrains.python.psi.resolve.fromModule import com.jetbrains.python.psi.resolve.resolveModuleAt +import java.util.* /** * @author Ilya.Kazakevich @@ -106,3 +115,45 @@ private fun findVFSItemRoot(virtualFile: VirtualFile, project: Project): Virtual } + +/** + * Emulates command line processor by parsing command line to arguments that can be provided as argv. + * Escape chars are not supported but quotes work. + * @throws ExecutionException if can't be parsed + */ +fun getParsedAdditionalArguments(project: Project, additionalArguments: String): List { + val factory = PsiFileFactory.getInstance(project) + val file = factory.createFileFromText(CommandLineLanguage.INSTANCE, + String.format("fake_command %s", additionalArguments)) as CommandLineFile + + if (file.children.any { it is PsiErrorElement }) { + throw ExecutionException("Additional arguments can't be parsed. Please check they are valid: $additionalArguments") + } + + + val additionalArgsList = ArrayList() + var skipArgument = false + file.children.filterIsInstance(CommandLinePart::class.java).forEach { + when (it) { + is CommandLineOption -> { + val optionText = it.text + val possibleArgument = it.findArgument() + if (possibleArgument != null) { + additionalArgsList.add(optionText + possibleArgument.valueNoQuotes) + skipArgument = true + } + else { + additionalArgsList.add(optionText) + } + } + is CommandLineArgument -> { + if (!skipArgument) { + additionalArgsList.add(it.valueNoQuotes) + } + skipArgument = false + } + } + } + return additionalArgsList +} + diff --git a/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt b/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt index 3a7bf25f66d2..ae0f70cb53f8 100644 --- a/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt +++ b/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt @@ -385,7 +385,7 @@ abstract class PyUniversalTestConfiguration(project: Project, private fun generateRawArguments(): List { val rawArguments = additionalArguments + " " + getCustomRawArgumentsString() if (rawArguments.isNotBlank()) { - return listOf("--") + rawArguments.trim().split(" ") + return listOf("--") + getParsedAdditionalArguments(project, additionalArguments) } return emptyList() } diff --git a/python/testData/commandLine/spaces.cmdline b/python/testData/commandLine/spaces.cmdline new file mode 100644 index 000000000000..a968b0fece4f --- /dev/null +++ b/python/testData/commandLine/spaces.cmdline @@ -0,0 +1 @@ +fake_command "spam and eggs" chicken --with=ketchup --with='russian mustard' \ No newline at end of file diff --git a/python/testData/commandLine/spaces.txt b/python/testData/commandLine/spaces.txt new file mode 100644 index 000000000000..b08603e6c11c --- /dev/null +++ b/python/testData/commandLine/spaces.txt @@ -0,0 +1,21 @@ +FILE + CommandLineCommandImpl(COMMAND) + PsiElement(LITERAL_STARTS_FROM_LETTER)('fake_command') + PsiWhiteSpace(' ') + CommandLineArgumentImpl(ARGUMENT) + PsiElement(SPACED_LITERAL_STARTS_FROM_LETTER)('"spam and eggs"') + PsiWhiteSpace(' ') + CommandLineArgumentImpl(ARGUMENT) + PsiElement(LITERAL_STARTS_FROM_LETTER)('chicken') + PsiWhiteSpace(' ') + CommandLineOptionImpl(OPTION) + PsiElement(LONG_OPTION_NAME_TOKEN)('--with') + PsiElement(=)('=') + CommandLineArgumentImpl(ARGUMENT) + PsiElement(LITERAL_STARTS_FROM_LETTER)('ketchup') + PsiWhiteSpace(' ') + CommandLineOptionImpl(OPTION) + PsiElement(LONG_OPTION_NAME_TOKEN)('--with') + PsiElement(=)('=') + CommandLineArgumentImpl(ARGUMENT) + PsiElement(SINGLE_Q_SPACED_LITERAL_STARTS_FROM_LETTER)(''russian mustard'') \ No newline at end of file diff --git a/python/testData/testRunner/env/nose/test_with_slow/test_with_slow.py b/python/testData/testRunner/env/nose/test_with_slow/test_with_slow.py new file mode 100644 index 000000000000..fbccf0507e42 --- /dev/null +++ b/python/testData/testRunner/env/nose/test_with_slow/test_with_slow.py @@ -0,0 +1,8 @@ +from nose.plugins.attrib import attr + +def test_fast(): + pass + +@attr('slow') +def test_Slow(): + pass \ No newline at end of file diff --git a/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py b/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py new file mode 100644 index 000000000000..a7b9646bbd78 --- /dev/null +++ b/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py @@ -0,0 +1,11 @@ + + +import pytest + +@pytest.mark.slow +def test_slow(): + pass + + +def test_fast(): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/commandInterface/commandLine/CommandLineParserTest.java b/python/testSrc/com/jetbrains/commandInterface/commandLine/CommandLineParserTest.java index f14d8be5dcdc..9f8996f3c07c 100644 --- a/python/testSrc/com/jetbrains/commandInterface/commandLine/CommandLineParserTest.java +++ b/python/testSrc/com/jetbrains/commandInterface/commandLine/CommandLineParserTest.java @@ -17,6 +17,9 @@ package com.jetbrains.commandInterface.commandLine; import com.intellij.testFramework.ParsingTestCase; +import com.jetbrains.commandInterface.commandLine.psi.CommandLineArgument; +import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile; +import org.junit.Assert; /** @@ -34,6 +37,14 @@ public final class CommandLineParserTest extends ParsingTestCase { return CommandTestTools.TEST_PATH; } + public void testSpaces() throws Exception { + doTest(true); + final CommandLineFile commandLineFile = (CommandLineFile)myFile; + Assert.assertEquals("Bad argument value", "spam and eggs", commandLineFile.getArguments().iterator().next().getValueNoQuotes()); + final CommandLineArgument optionArgument = commandLineFile.getOptions().iterator().next().findArgument(); + Assert.assertNotNull("No option argument found", optionArgument); + Assert.assertEquals("Bad option argument value", "ketchup", optionArgument.getValueNoQuotes()); + } /** * Should be ok diff --git a/python/testSrc/com/jetbrains/env/python/testing/PythonNoseTestingTest.java b/python/testSrc/com/jetbrains/env/python/testing/PythonNoseTestingTest.java index abd1870f47b0..839ee3ede14a 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/PythonNoseTestingTest.java +++ b/python/testSrc/com/jetbrains/env/python/testing/PythonNoseTestingTest.java @@ -6,11 +6,14 @@ import com.jetbrains.env.PyEnvTestCase; import com.jetbrains.env.PyProcessWithConsoleTestTask; import com.jetbrains.env.python.testing.CreateConfigurationTestTask.PyConfigurationCreationTask; import com.jetbrains.env.ut.PyNoseTestProcessRunner; +import com.jetbrains.env.ut.PyTestTestProcessRunner; import com.jetbrains.python.sdkTools.SdkCreationType; import com.jetbrains.python.testing.PythonTestConfigurationsModel; import com.jetbrains.python.testing.universalTests.PyUniversalNoseTestConfiguration; import com.jetbrains.python.testing.universalTests.PyUniversalNoseTestFactory; +import com.jetbrains.python.testing.universalTests.PyUniversalPyTestConfiguration; import org.jetbrains.annotations.NotNull; +import org.junit.Assert; import org.junit.Test; import java.io.IOException; @@ -23,6 +26,41 @@ import static org.junit.Assert.assertEquals; @EnvTestTagsRequired(tags = "nose") public final class PythonNoseTestingTest extends PyEnvTestCase { + + // Ensure slow test is not run when --attr="!slow" is provided + @Test + public void testMarkerWithSlow() throws Exception { + runPythonTest( + new PyProcessWithConsoleTestTask("/testRunner/env/nose/test_with_slow", SdkCreationType.EMPTY_SDK) { + + @NotNull + @Override + protected PyNoseTestProcessRunner createProcessRunner() throws Exception { + return new PyNoseTestProcessRunner("test_with_slow.py", 0) { + @Override + protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalNoseTestConfiguration configuration) throws IOException { + super.configurationCreatedAndWillLaunch(configuration); + configuration.setAdditionalArguments("--attr=\"!slow\""); + } + }; + } + + + @Override + protected void checkTestResults(@NotNull PyNoseTestProcessRunner runner, + @NotNull String stdout, + @NotNull String stderr, + @NotNull String all) { + Assert.assertEquals("--slow runner borken", "Test tree:\n" + + "[root]\n" + + ".test_with_slow\n" + + "..test_fast(+)\n", + runner.getFormattedTestTree()); + } + }); + } + + @Test public void testMultipleCases() throws Exception { runPythonTest( diff --git a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java index afc0870db292..a7cb9f2715cd 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java +++ b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java @@ -31,6 +31,39 @@ import static org.junit.Assert.assertEquals; @EnvTestTagsRequired(tags = "pytest") public final class PythonPyTestingTest extends PyEnvTestCase { + // Ensure slow test is not run when -m "not slow" is provided + @Test + public void testMarkerWithSpaces() throws Exception { + runPythonTest( + new PyProcessWithConsoleTestTask("/testRunner/env/pytest/test_with_markers", SdkCreationType.EMPTY_SDK) { + + @NotNull + @Override + protected PyTestTestProcessRunner createProcessRunner() throws Exception { + return new PyTestTestProcessRunner("test_with_markers.py", 0) { + @Override + protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException { + super.configurationCreatedAndWillLaunch(configuration); + configuration.setAdditionalArguments("-m 'not slow'"); + } + }; + } + + + @Override + protected void checkTestResults(@NotNull PyTestTestProcessRunner runner, + @NotNull String stdout, + @NotNull String stderr, + @NotNull String all) { + Assert.assertEquals("Marker support broken", "Test tree:\n" + + "[root]\n" + + ".test_with_markers\n" + + "..test_fast(+)\n", + runner.getFormattedTestTree()); + } + }); + } + @Test public void testConfigurationProducer() throws Exception { runPythonTest( @@ -49,7 +82,8 @@ public final class PythonPyTestingTest extends PyEnvTestCase { @Test public void testTestsInSubFolderResolvable() throws Exception { runPythonTest( - new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner("test_metheggs", "test_funeggs", "test_first") { + new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner("test_metheggs", "test_funeggs", + "test_first") { @NotNull @Override protected PyTestTestProcessRunner createProcessRunner() throws Exception { @@ -110,7 +144,8 @@ public final class PythonPyTestingTest extends PyEnvTestCase { @Test public void testProduceConfigurationOnFile() throws Exception { runPythonTest( - new CreateConfigurationByFileTask(PythonTestConfigurationsModel.PY_TEST_NAME, PyUniversalPyTestConfiguration.class, "spam.py") { + new CreateConfigurationByFileTask(PythonTestConfigurationsModel.PY_TEST_NAME, + PyUniversalPyTestConfiguration.class, "spam.py") { @NotNull @Override protected PsiElement getElementToRightClickOnByFile(@NotNull final String fileName) { @@ -246,7 +281,7 @@ public final class PythonPyTestingTest extends PyEnvTestCase { if (getLevelForSdk().isPy3K()) { return new PyTestTestProcessRunner("folder_no_init_py/test_test.py", 2); } - else { + else { return new PyTestTestProcessRunner(toFullPath("folder_no_init_py/test_test.py"), 2) { @Override protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException { diff --git a/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt b/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt new file mode 100644 index 000000000000..d6fc8a1c6ba1 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2017 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.jetbrains.python.testing.universalTests + +import com.jetbrains.python.fixtures.PyTestCase +import org.junit.Assert +import org.junit.Test + +/** + * @author Ilya.Kazakevich + */ +class PyTestRunnerUtilsKtTest : PyTestCase() { + @Test + fun testGetParsedAdditionalArguments() { + var list = getParsedAdditionalArguments(myFixture.project, "-v --color=red -m 'spam and eggs'") + Assert.assertEquals("List parsed incorrectly", listOf("-v", "--color=red", "-m", "spam and eggs"), list) + + list = getParsedAdditionalArguments(myFixture.project, "--eggs=spam --foo=\"eggs and spam\"") + Assert.assertEquals("List parsed incorrectly", listOf("--eggs=spam", "--foo=eggs and spam"), list) + + } +} \ No newline at end of file diff --git a/resources/src/idea/RichPlatformPlugin.xml b/resources/src/idea/RichPlatformPlugin.xml index 6dbb8498fa2c..8b8fcefcfd6a 100644 --- a/resources/src/idea/RichPlatformPlugin.xml +++ b/resources/src/idea/RichPlatformPlugin.xml @@ -304,7 +304,6 @@ - diff --git a/xml/tests/src/com/intellij/psi/formatter/HtmlFormatterTest.java b/xml/tests/src/com/intellij/psi/formatter/HtmlFormatterTest.java index 23fa64bc1b30..4bd980e2dc2a 100644 --- a/xml/tests/src/com/intellij/psi/formatter/HtmlFormatterTest.java +++ b/xml/tests/src/com/intellij/psi/formatter/HtmlFormatterTest.java @@ -262,6 +262,13 @@ public class HtmlFormatterTest extends XmlFormatterTestBase { doTestPerformance("html reformat with range", 5000, null); } + public void testQuotesReplacementPerformance() throws Exception { + doTestPerformance("Quotes replacement", 1500, (settings)->{ + settings.HTML_QUOTE_STYLE = CodeStyleSettings.QuoteStyle.Single; + settings.HTML_ENFORCE_QUOTES = true; + }); + } + public void doTestPerformance(@NotNull String message, int expectedTime, OptionsSetup optionsSetup) throws Exception { CodeStyleSettings settings = new CodeStyleSettings(); if (optionsSetup != null) optionsSetup.setupOptions(settings); @@ -505,13 +512,6 @@ public class HtmlFormatterTest extends XmlFormatterTestBase { } } - public void testQuotesReplacementPerformance() throws Exception { - doTestPerformance("Quotes replacement", 1500, (settings)->{ - settings.HTML_QUOTE_STYLE = CodeStyleSettings.QuoteStyle.Single; - settings.HTML_ENFORCE_QUOTES = true; - }); - } - public void testWeb18213() { doTextTest( "

\n" + diff --git a/xml/tests/testData/psi/formatter/html/performance.html b/xml/tests/testData/psi/formatter/html/performance.html deleted file mode 100644 index c6ce8b123a8c..000000000000 --- a/xml/tests/testData/psi/formatter/html/performance.html +++ /dev/null @@ -1 +0,0 @@ -yandeks

segodnya v novostyach 16:58

  1. 1. k 12 godam katorzhnych rabot prigovoreny dve amerikanskie zhurnalistki
  2. 2. najdeno 17 tel s razbivshegosya v atlantike aerobusa Air France
  3. 3. sud prigovoril ubijcu moskovskoj shkoljnicy k 23 godam lisheniya svobody
  4. 4. kazhdyj desyatyj rossijskij vypusknik provalil ege po geografii
  5. 5. dmitrij mezencev utverzhden na postu gubernatora �?rkutskoj oblasti
  1. 1. General Motors na tri nedeli ostanovil konvejer peterburgskogo zavoda
  2. 2. v peterburge vruczili premiyu «nacionaljnyj bestseller»
  3. 3. fejerverka na denj rossii v peterburge ne budet
  4. 4. czinovnikov krasnogvardejskogo rajona nakazali za korotkie peremeny v shkolach
  5. 5. russkij muzej pokazhet sto shedevrov na ulicach peterburga

esche boljshe novych vakansij v pitere

moj krug — novyj vitok vashej karjery

yandeks

poiskkartymarketnovostislovariblogivideokartinki
rasshirennyj poisk

poczta

zavesti pocztovyj yaschik

\ No newline at end of file diff --git a/xml/tests/testData/psi/formatter/html/performance_after.html b/xml/tests/testData/psi/formatter/html/performance_after.html deleted file mode 100644 index 28b5e3d3b362..000000000000 --- a/xml/tests/testData/psi/formatter/html/performance_after.html +++ /dev/null @@ -1,372 +0,0 @@ - -yandeks - - - - - - - - - - - - -
- - -

segodnya v novostyach 16:58

- -
-
    -
  1. 1. k 12 godam katorzhnych rabot prigovoreny - dve amerikanskie zhurnalistki -
  2. 2. najdeno 17 tel s razbivshegosya v - atlantike aerobusa Air France -
  3. 3. sud prigovoril ubijcu moskovskoj shkoljnicy k - 23 godam lisheniya svobody -
  4. 4. kazhdyj desyatyj rossijskij vypusknik provalil - ege po geografii -
  5. 5. dmitrij mezencev utverzhden na - postu gubernatora �?rkutskoj oblasti -
-
-
-
    -
  1. 1. General Motors na tri nedeli ostanovil - konvejer peterburgskogo zavoda -
  2. 2. v peterburge vruczili premiyu - «nacionaljnyj bestseller» -
  3. 3. fejerverka na denj rossii - v peterburge ne budet -
  4. 4. czinovnikov krasnogvardejskogo rajona nakazali - za korotkie peremeny v shkolach -
  5. 5. russkij muzej pokazhet sto - shedevrov na ulicach peterburga -
-
-
- - -
- - -
-

- esche boljshe novych vakansij v pitere

-

moj krug — novyj vitok vashej karjery

-
-

yandeks

-
-
- - -
poisk - karty - market - novosti - slovari - blogi - video - kartinki - - -
- - -
- - - -
- -
- - rasshirennyj poisk -
-
- -
-
- - - - -
-
-

poczta

-
-
-
-
-
- - -
- - - -
-

- zavesti - pocztovyj yaschik

-
-

fotki

- - -
-
-
-
fotka
-
-
-
-
-
- - -
- -
-
-
- \ No newline at end of file diff --git a/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance.html b/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance.html deleted file mode 100644 index f0768855a57a..000000000000 --- a/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance.html +++ /dev/null @@ -1,5213 +0,0 @@ - - - - JavaScript Courses - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- -
- -
- - -
- -

- Welcome to the acme Library -

- -
-
-

- This is a sample catalog of all the courses we offer. Browse by topic or difficulty. Sign up - today and get access to our entire library. acme students get access to workshops, bonus - content, conferences, and more. -

-

Ready to start learning?

-

- acme offers a 7 day free trial for new students. Get access to 1000s of - hours of content. Learn to code, land your dream job. -

- Start - Your Free Trial - - - - -
-
- Welcome -
-
- -
- - -
- - - - - -
-
-

JavaScript

-

JavaScript is a programming language that allows you to add - interactivity to websites. It can be used to create interactive effects on web pages.

-
- - -
- -
- -
-

Whoops! Perhaps you can try a broader search.

-

Reset all filters

-
-
-
-

Upcoming Releases

-

The following items are scheduled to be released soon. You can also visit our content roadmap for more info.

-
- -
- - -
- -
- -
-
- -
- - - -
- - - - - - - - -
- - -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance_after.html b/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance_after.html deleted file mode 100644 index 9213559283aa..000000000000 --- a/xml/tests/testData/psi/formatter/html/quotesReplacementPerformance_after.html +++ /dev/null @@ -1,5231 +0,0 @@ - - - - JavaScript Courses - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- -
- -
- - -
- -

- Welcome to the acme Library -

- -
-
-

- This is a sample catalog of all the courses we offer. Browse by topic or difficulty. Sign up - today and get access to our entire library. acme students get access to workshops, bonus - content, conferences, and more. -

-

Ready to start learning?

-

- acme offers a 7 day free trial for new students. Get access to 1000s of - hours of content. Learn to code, land your dream job. -

- Start - Your Free Trial - - - - -
-
- Welcome -
-
- -
- - -
- - - - - -
-
-

JavaScript

-

JavaScript is a programming language that allows you to add - interactivity to websites. It can be used to create interactive effects on web pages.

-
- - -
- -
- -
-

Whoops! Perhaps you can try a broader search.

-

Reset all filters

-
-
-
-

Upcoming Releases

-

The following items are scheduled to be released soon. You can also visit our content roadmap for more info.

-
- -
- - -
- -
- -
-
- -
- - - -
- - - - - - - - -
- - -
- - - - - - - - - - - - - \ No newline at end of file