| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * 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.android.tools.idea.welcome.wizard; |
| |
| import com.android.annotations.VisibleForTesting; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.intellij.execution.process.ProcessAdapter; |
| import com.intellij.execution.process.ProcessEvent; |
| import com.intellij.execution.process.ProcessHandler; |
| import com.intellij.execution.ui.ConsoleViewContentType; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.LogicalPosition; |
| import com.intellij.openapi.editor.ScrollType; |
| import com.intellij.openapi.editor.colors.EditorColorsScheme; |
| import com.intellij.openapi.editor.event.DocumentAdapter; |
| import com.intellij.openapi.editor.highlighter.EditorHighlighter; |
| import com.intellij.openapi.editor.highlighter.HighlighterClient; |
| import com.intellij.openapi.editor.highlighter.HighlighterIterator; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.tree.IElementType; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** |
| * Keeps track of attributes for ranges and has some special support for tracking process output. |
| */ |
| public final class ConsoleHighlighter extends DocumentAdapter implements EditorHighlighter { |
| private List<HighlightRange> myRanges = Lists.newArrayListWithCapacity(1024); |
| private boolean myIsUpdatePending = false; |
| private StringBuilder myPendingStrings = new StringBuilder(4096); |
| private HighlighterClient myEditor; |
| private ModalityState myModalityState = ModalityState.defaultModalityState(); |
| |
| public synchronized void print(String string, @Nullable TextAttributes attributes) { |
| Application application = ApplicationManager.getApplication(); |
| myPendingStrings.append(string); |
| if (!myIsUpdatePending && application != null && !application.isUnitTestMode()) { |
| myIsUpdatePending = true; |
| application.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| appendToDocument(); |
| } |
| }, myModalityState); |
| } |
| |
| HighlightRange lastRange = Iterables.getLast(myRanges, HighlightRange.EMPTY); |
| assert lastRange != null; |
| int start = lastRange.end; |
| myRanges.add(new HighlightRange(start, start + string.length(), attributes)); |
| } |
| |
| public void setModalityState(ModalityState state) { |
| myModalityState = state; |
| } |
| |
| /** |
| * Code that requires locking and will be executed on UI thread. We should |
| * not do long-running UI operations (e.g. document append) under the lock. |
| */ |
| private synchronized String getPendingString() { |
| String string = myPendingStrings.toString(); |
| myPendingStrings.delete(0, string.length()); |
| myIsUpdatePending = false; |
| return string; |
| } |
| |
| private void appendToDocument() { |
| Document document = myEditor.getDocument(); |
| if (document != null) { |
| String pendingString = StringUtil.convertLineSeparators(getPendingString()); |
| document.insertString(document.getTextLength(), pendingString); |
| if (myEditor instanceof Editor) { |
| Editor editor = (Editor)myEditor; |
| int lineCount = document.getLineCount(); |
| editor.getScrollingModel().scrollTo(new LogicalPosition(lineCount - 1, 0), ScrollType.MAKE_VISIBLE); |
| } |
| } |
| } |
| |
| @NotNull |
| @Override |
| public HighlighterIterator createIterator(int startOffset) { |
| return new HighlightedRangesIterator(getOffsetRangeIndex(startOffset)); |
| } |
| |
| @Override |
| public void setText(@NotNull CharSequence text) { |
| |
| } |
| |
| @Override |
| public void setEditor(@NotNull HighlighterClient editor) { |
| myEditor = editor; |
| } |
| |
| @Override |
| public void setColorScheme(@NotNull EditorColorsScheme scheme) { |
| |
| } |
| |
| private synchronized HighlightRange getRange(int index) { |
| if (index < 0 || index >= myRanges.size()) { |
| return HighlightRange.EMPTY; |
| } |
| else { |
| return myRanges.get(index); |
| } |
| } |
| |
| @VisibleForTesting |
| synchronized int getOffsetRangeIndex(int startOffset) { |
| if (myRanges.isEmpty() || startOffset < 0 || startOffset >= Iterables.getLast(myRanges).end) { |
| return -1; |
| } |
| int end = myRanges.size(); |
| int i = end / 2; |
| while (true) { |
| HighlightRange range = myRanges.get(i); |
| if (range.end > startOffset) { |
| if (range.start <= startOffset) { |
| return i; |
| } |
| else { |
| end = i; |
| i /= 2; |
| } |
| } |
| else { |
| i = (i + end) / 2; |
| } |
| } |
| } |
| |
| public void clear() { |
| clearHighlightedState(); |
| myEditor.getDocument().setText(""); |
| } |
| |
| private synchronized void clearHighlightedState() { |
| myRanges.clear(); |
| myPendingStrings.delete(0, myPendingStrings.length() - 1); |
| myIsUpdatePending = false; |
| } |
| |
| public void attachToProcess(ProcessHandler processHandler) { |
| processHandler.addProcessListener(new ProcessOutputProcessor()); |
| } |
| |
| private static final class HighlightRange { |
| public final static HighlightRange EMPTY = new HighlightRange(0, 0, null); |
| |
| @Nullable public final TextAttributes attributes; |
| public final int start; |
| public final int end; |
| |
| public HighlightRange(int start, int end, @Nullable TextAttributes attributes) { |
| this.attributes = attributes; |
| this.start = start; |
| this.end = end; |
| } |
| } |
| |
| private class ProcessOutputProcessor extends ProcessAdapter { |
| private AtomicBoolean mySkipped = new AtomicBoolean(false); |
| |
| @Override |
| public void onTextAvailable(final ProcessEvent event, final Key outputType) { |
| if (!mySkipped.compareAndSet(false, true)) { |
| print(event.getText(), ConsoleViewContentType.getConsoleViewType(outputType).getAttributes()); |
| } |
| } |
| } |
| |
| private class HighlightedRangesIterator implements HighlighterIterator { |
| private int myIndex; |
| @NotNull private HighlightRange myRange = HighlightRange.EMPTY; |
| |
| public HighlightedRangesIterator(int index) { |
| myIndex = index; |
| myRange = getRange(index); |
| } |
| |
| @Nullable |
| @Override |
| public TextAttributes getTextAttributes() { |
| return myRange.attributes; |
| } |
| |
| @Override |
| public int getStart() { |
| return myRange.start; |
| } |
| |
| @Override |
| public int getEnd() { |
| return myRange.end; |
| } |
| |
| @Nullable |
| @Override |
| public IElementType getTokenType() { |
| return null; |
| } |
| |
| @Override |
| public void advance() { |
| myRange = getRange(++myIndex); |
| } |
| |
| @Override |
| public void retreat() { |
| myRange = getRange(--myIndex); |
| } |
| |
| @Override |
| public boolean atEnd() { |
| return myRange == HighlightRange.EMPTY; |
| } |
| |
| @Override |
| public Document getDocument() { |
| return myEditor.getDocument(); |
| } |
| } |
| } |