blob: 2b142a4bffb2af40f4094c31ab213a55e172085e [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.application.options;
import com.intellij.application.options.codeStyle.CodeStyleSchemesModel;
import com.intellij.application.options.codeStyle.LanguageSelector;
import com.intellij.codeStyle.CodeStyleFacade;
import com.intellij.lang.Language;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.codeStyle.*;
import com.intellij.ui.UserActivityListener;
import com.intellij.ui.UserActivityWatcher;
import com.intellij.util.Alarm;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.ui.update.UiNotifyConnector;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
public abstract class CodeStyleAbstractPanel implements Disposable {
private static final long TIME_TO_HIGHLIGHT_PREVIEW_CHANGES_IN_MILLIS = TimeUnit.SECONDS.toMillis(3);
private static final Logger LOG = Logger.getInstance("#com.intellij.application.options.CodeStyleXmlPanel");
private final ChangesDiffCalculator myDiffCalculator = new ChangesDiffCalculator();
private final List<TextRange> myPreviewRangesToHighlight = new ArrayList<TextRange>();
private final Editor myEditor;
private final CodeStyleSettings mySettings;
private boolean myShouldUpdatePreview;
protected static final int[] ourWrappings =
{CommonCodeStyleSettings.DO_NOT_WRAP, CommonCodeStyleSettings.WRAP_AS_NEEDED, CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM, CommonCodeStyleSettings.WRAP_ALWAYS};
private long myLastDocumentModificationStamp;
private String myTextToReformat = null;
private final UserActivityWatcher myUserActivityWatcher = new UserActivityWatcher();
private final Alarm myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
private CodeStyleSchemesModel myModel;
private boolean mySomethingChanged = false;
private long myEndHighlightPreviewChangesTimeMillis = -1;
private boolean myShowsPreviewHighlighters;
private boolean mySkipPreviewHighlighting;
private LanguageSelector myLanguageSelector;
private final CodeStyleSettings myCurrentSettings;
private final Language myDefaultLanguage;
protected CodeStyleAbstractPanel(@NotNull CodeStyleSettings settings) {
this(null, null, settings);
}
protected CodeStyleAbstractPanel(@Nullable Language defaultLanguage,
@Nullable CodeStyleSettings currentSettings,
@NotNull CodeStyleSettings settings)
{
Disposer.register(this, myDiffCalculator);
myCurrentSettings = currentSettings;
mySettings = settings;
myDefaultLanguage = defaultLanguage;
myEditor = createEditor();
if (myEditor != null) {
myUpdateAlarm.setActivationComponent(myEditor.getComponent());
}
myUserActivityWatcher.addUserActivityListener(new UserActivityListener() {
@Override
public void stateChanged() {
somethingChanged();
}
});
updatePreview(true);
}
protected void setShouldUpdatePreview(boolean shouldUpdatePreview) {
myShouldUpdatePreview = shouldUpdatePreview;
}
private synchronized void setSomethingChanged(final boolean b) {
mySomethingChanged = b;
}
private synchronized boolean isSomethingChanged() {
return mySomethingChanged;
}
public void setModel(final CodeStyleSchemesModel model) {
myModel = model;
}
protected void somethingChanged() {
if (myModel != null) {
myModel.fireCurrentSettingsChanged();
}
}
protected void addPanelToWatch(Component component) {
myUserActivityWatcher.register(component);
}
@Nullable
private Editor createEditor() {
if (getPreviewText() == null) return null;
EditorFactory editorFactory = EditorFactory.getInstance();
Document editorDocument = editorFactory.createDocument("");
EditorEx editor = (EditorEx)editorFactory.createEditor(editorDocument);
fillEditorSettings(editor.getSettings());
myLastDocumentModificationStamp = editor.getDocument().getModificationStamp();
return editor;
}
private static void fillEditorSettings(final EditorSettings editorSettings) {
editorSettings.setWhitespacesShown(true);
editorSettings.setLineMarkerAreaShown(false);
editorSettings.setIndentGuidesShown(false);
editorSettings.setLineNumbersShown(false);
editorSettings.setFoldingOutlineShown(false);
editorSettings.setAdditionalColumnsCount(0);
editorSettings.setAdditionalLinesCount(1);
editorSettings.setUseSoftWraps(false);
}
protected void updatePreview(boolean useDefaultSample) {
if (myEditor == null) return;
updateEditor(useDefaultSample);
updatePreviewHighlighter((EditorEx)myEditor);
}
private void updateEditor(boolean useDefaultSample) {
if (!myShouldUpdatePreview || (!ApplicationManager.getApplication().isUnitTestMode() && !myEditor.getComponent().isShowing())) {
return;
}
if (myLastDocumentModificationStamp != myEditor.getDocument().getModificationStamp()) {
myTextToReformat = myEditor.getDocument().getText();
}
else if (useDefaultSample || myTextToReformat == null) {
myTextToReformat = getPreviewText();
}
int currOffs = myEditor.getScrollingModel().getVerticalScrollOffset();
final Project finalProject = ProjectUtil.guessCurrentProject(getPanel());
CommandProcessor.getInstance().executeCommand(finalProject, new Runnable() {
@Override
public void run() {
replaceText(finalProject);
}
}, null, null);
myEditor.getSettings().setRightMargin(getAdjustedRightMargin());
myLastDocumentModificationStamp = myEditor.getDocument().getModificationStamp();
myEditor.getScrollingModel().scrollVertically(currOffs);
}
private int getAdjustedRightMargin() {
int result = getRightMargin();
return result > 0 ? result : CodeStyleFacade.getInstance(ProjectUtil.guessCurrentProject(getPanel())).getRightMargin(getDefaultLanguage());
}
protected abstract int getRightMargin();
private void replaceText(final Project project) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
try {
Document beforeReformat = null;
if (!mySkipPreviewHighlighting) {
beforeReformat = collectChangesBeforeCurrentSettingsAppliance(project);
}
//important not mark as generated not to get the classes before setting language level
PsiFile psiFile = createFileFromText(project, myTextToReformat);
prepareForReformat(psiFile);
try {
apply(mySettings);
}
catch (ConfigurationException ignore) {
}
CodeStyleSettings clone = mySettings.clone();
clone.setRightMargin(getDefaultLanguage(), getAdjustedRightMargin());
CodeStyleSettingsManager.getInstance(project).setTemporarySettings(clone);
PsiFile formatted;
try {
formatted = doReformat(project, psiFile);
}
finally {
CodeStyleSettingsManager.getInstance(project).dropTemporarySettings();
}
myEditor.getSettings().setTabSize(clone.getTabSize(getFileType()));
Document document = myEditor.getDocument();
document.replaceString(0, document.getTextLength(), formatted.getText());
if (document != null && beforeReformat != null) {
highlightChanges(beforeReformat);
}
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
});
}
/**
* Reformats {@link #myTextToReformat target text} with the {@link #mySettings current code style settings} and returns
* list of changes applied to the target text during that.
*
* @param project project to use
* @return list of changes applied to the {@link #myTextToReformat target text} during reformatting. It is sorted
* by change start offset in ascending order
*/
@Nullable
private Document collectChangesBeforeCurrentSettingsAppliance(Project project) {
PsiFile psiFile = createFileFromText(project, myTextToReformat);
prepareForReformat(psiFile);
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
if (documentManager != null) {
Document document = documentManager.getDocument(psiFile);
if (document != null) {
CodeStyleSettings clone = mySettings.clone();
clone.setRightMargin(getDefaultLanguage(), getAdjustedRightMargin());
CodeStyleSettingsManager.getInstance(project).setTemporarySettings(clone);
try {
CodeStyleManager.getInstance(project).reformat(psiFile);
}
finally {
CodeStyleSettingsManager.getInstance(project).dropTemporarySettings();
}
return document;
}
}
return null;
}
public void setSkipPreviewHighlighting(boolean skipPreviewHighlighting) {
mySkipPreviewHighlighting = skipPreviewHighlighting;
}
protected void prepareForReformat(PsiFile psiFile) {
}
protected String getFileExt() {
return getFileTypeExtension(getFileType());
}
protected PsiFile createFileFromText(Project project, String text) {
return PsiFileFactory.getInstance(project).createFileFromText(
"a." + getFileExt(), getFileType(), text, LocalTimeCounter.currentTime(), true
);
}
protected PsiFile doReformat(final Project project, final PsiFile psiFile) {
CodeStyleManager.getInstance(project).reformat(psiFile);
return psiFile;
}
private void highlightChanges(Document beforeReformat) {
if (mySkipPreviewHighlighting) {
return;
}
myPreviewRangesToHighlight.clear();
MarkupModel markupModel = myEditor.getMarkupModel();
markupModel.removeAllHighlighters();
int textLength = myEditor.getDocument().getTextLength();
boolean highlightPreview = false;
Collection<TextRange> ranges = myDiffCalculator.calculateDiff(beforeReformat, myEditor.getDocument());
for (TextRange range : ranges) {
if (range.getStartOffset() >= textLength) {
continue;
}
highlightPreview = true;
TextRange rangeToUse = calculateChangeHighlightRange(range);
myPreviewRangesToHighlight.add(rangeToUse);
}
if (highlightPreview) {
myEndHighlightPreviewChangesTimeMillis = System.currentTimeMillis() + TIME_TO_HIGHLIGHT_PREVIEW_CHANGES_IN_MILLIS;
myShowsPreviewHighlighters = true;
}
}
/**
* Allows to answer if particular visual position belongs to visual rectangle identified by the given visual position of
* its top-left and bottom-right corners.
*
* @param targetPosition position which belonging to target visual rectangle should be checked
* @param startPosition visual position of top-left corner of the target visual rectangle
* @param endPosition visual position of bottom-right corner of the target visual rectangle
* @return <code>true</code> if given visual position belongs to the target visual rectangle;
* <code>false</code> otherwise
*/
private static boolean isWithinBounds(VisualPosition targetPosition, VisualPosition startPosition, VisualPosition endPosition) {
return targetPosition.line >= startPosition.line && targetPosition.line <= endPosition.line
&& targetPosition.column >= startPosition.column && targetPosition.column <= endPosition.column;
}
/**
* We want to highlight document formatting changes introduced by particular formatting property value change.
* However, there is a possible effect that white space region is removed. We still want to highlight that, hence, it's necessary
* to highlight neighbour region.
* <p/>
* This method encapsulates logic of adjusting preview highlight change if necessary.
*
* @param range initial range to highlight
* @return resulting range to highlight
*/
private TextRange calculateChangeHighlightRange(TextRange range) {
CharSequence text = myEditor.getDocument().getCharsSequence();
if (range.getLength() <= 0) {
int offset = range.getStartOffset();
while (offset < text.length() && text.charAt(offset) == ' ') {
offset++;
}
return offset > range.getStartOffset() ? new TextRange(offset, offset) : range;
}
int startOffset = range.getStartOffset() + 1;
int endOffset = range.getEndOffset() + 1;
boolean useSameRange = true;
while (endOffset <= text.length()
&& StringUtil.equals(text.subSequence(range.getStartOffset(), range.getEndOffset()), text.subSequence(startOffset, endOffset)))
{
useSameRange = false;
startOffset++;
endOffset++;
}
startOffset--;
endOffset--;
return useSameRange ? range : new TextRange(startOffset, endOffset);
}
private void updatePreviewHighlighter(final EditorEx editor) {
EditorColorsScheme scheme = editor.getColorsScheme();
scheme.setColor(EditorColors.CARET_ROW_COLOR, null);
editor.setHighlighter(createHighlighter(scheme));
}
@Nullable
protected abstract EditorHighlighter createHighlighter(final EditorColorsScheme scheme);
@NotNull
protected abstract FileType getFileType();
@NonNls
@Nullable
protected abstract String getPreviewText();
public abstract void apply(CodeStyleSettings settings) throws ConfigurationException;
public final void reset(final CodeStyleSettings settings) {
myShouldUpdatePreview = false;
try {
resetImpl(settings);
}
finally {
myShouldUpdatePreview = true;
}
}
protected static int getIndexForWrapping(int value) {
for (int i = 0; i < ourWrappings.length; i++) {
int ourWrapping = ourWrappings[i];
if (ourWrapping == value) return i;
}
LOG.assertTrue(false);
return 0;
}
public abstract boolean isModified(CodeStyleSettings settings);
@Nullable
public abstract JComponent getPanel();
@Override
public void dispose() {
myUpdateAlarm.cancelAllRequests();
if (myEditor != null) {
EditorFactory.getInstance().releaseEditor(myEditor);
}
}
protected abstract void resetImpl(final CodeStyleSettings settings);
protected static void fillWrappingCombo(final JComboBox wrapCombo) {
wrapCombo.addItem(ApplicationBundle.message("wrapping.do.not.wrap"));
wrapCombo.addItem(ApplicationBundle.message("wrapping.wrap.if.long"));
wrapCombo.addItem(ApplicationBundle.message("wrapping.chop.down.if.long"));
wrapCombo.addItem(ApplicationBundle.message("wrapping.wrap.always"));
}
public static String readFromFile(final Class resourceContainerClass, @NonNls final String fileName) {
try {
final InputStream stream = resourceContainerClass.getClassLoader().getResourceAsStream("codeStyle/preview/" + fileName);
final InputStreamReader reader = new InputStreamReader(stream);
final StringBuffer result;
final LineNumberReader lineNumberReader = new LineNumberReader(reader);
try {
result = new StringBuffer();
String line;
while ((line = lineNumberReader.readLine()) != null) {
result.append(line);
result.append("\n");
}
}
finally {
lineNumberReader.close();
}
return result.toString();
}
catch (IOException e) {
return "";
}
}
protected void installPreviewPanel(final JPanel previewPanel) {
previewPanel.setLayout(new BorderLayout());
previewPanel.add(myEditor.getComponent(), BorderLayout.CENTER);
}
@NonNls
protected
String getFileTypeExtension(FileType fileType) {
return fileType.getDefaultExtension();
}
public void onSomethingChanged() {
setSomethingChanged(true);
if (myEditor != null) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
updateEditor(true);
}
else {
UiNotifyConnector.doWhenFirstShown(myEditor.getComponent(), new Runnable() {
@Override
public void run() {
addUpdatePreviewRequest();
}
});
}
}
}
private void addUpdatePreviewRequest() {
myUpdateAlarm.addComponentRequest(new Runnable() {
@Override
public void run() {
try {
myUpdateAlarm.cancelAllRequests();
if (isSomethingChanged()) {
updateEditor(false);
}
if (System.currentTimeMillis() <= myEndHighlightPreviewChangesTimeMillis && !myPreviewRangesToHighlight.isEmpty()) {
blinkHighlighters();
myUpdateAlarm.addComponentRequest(this, 500);
}
else {
myEditor.getMarkupModel().removeAllHighlighters();
}
}
finally {
setSomethingChanged(false);
}
}
}, 300);
}
private void blinkHighlighters() {
MarkupModel markupModel = myEditor.getMarkupModel();
if (myShowsPreviewHighlighters) {
Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
VisualPosition visualStart = myEditor.xyToVisualPosition(visibleArea.getLocation());
VisualPosition visualEnd = myEditor.xyToVisualPosition(new Point(visibleArea.x + visibleArea.width, visibleArea.y + visibleArea.height));
// There is a possible case that viewport is located at its most bottom position and last document symbol
// is located at the start of the line, hence, resulting visual end column has a small value and doesn't actually
// indicates target visible rectangle. Hence, we need to correct that if necessary.
int endColumnCandidate = visibleArea.width / EditorUtil.getSpaceWidth(Font.PLAIN, myEditor) + visualStart.column;
if (endColumnCandidate > visualEnd.column) {
visualEnd = new VisualPosition(visualEnd.line, endColumnCandidate);
}
int offsetToScroll = -1;
CharSequence text = myEditor.getDocument().getCharsSequence();
TextAttributes backgroundAttributes = myEditor.getColorsScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
TextAttributes borderAttributes = new TextAttributes(
null, null, backgroundAttributes.getBackgroundColor(), EffectType.BOXED, Font.PLAIN
);
boolean scrollToChange = true;
for (TextRange range : myPreviewRangesToHighlight) {
if (scrollToChange) {
boolean rangeVisible = isWithinBounds(myEditor.offsetToVisualPosition(range.getStartOffset()), visualStart, visualEnd)
|| isWithinBounds(myEditor.offsetToVisualPosition(range.getEndOffset()), visualStart, visualEnd);
scrollToChange = !rangeVisible;
if (offsetToScroll < 0) {
if (offsetToScroll < 0) {
if (text.charAt(range.getStartOffset()) != '\n') {
offsetToScroll = range.getStartOffset();
}
else if (range.getEndOffset() > 0 && text.charAt(range.getEndOffset() - 1) != '\n') {
offsetToScroll = range.getEndOffset() - 1;
}
}
}
}
TextAttributes attributesToUse = range.getLength() > 0 ? backgroundAttributes : borderAttributes;
markupModel.addRangeHighlighter(
range.getStartOffset(), range.getEndOffset(), HighlighterLayer.SELECTION, attributesToUse, HighlighterTargetArea.EXACT_RANGE
);
}
if (scrollToChange) {
if (offsetToScroll < 0 && !myPreviewRangesToHighlight.isEmpty()) {
offsetToScroll = myPreviewRangesToHighlight.get(0).getStartOffset();
}
if (offsetToScroll >= 0 && offsetToScroll < text.length() - 1 && text.charAt(offsetToScroll) != '\n') {
// There is a possible case that target offset is located too close to the right edge. However, our point is to show
// highlighted region at target offset, hence, we need to scroll to the visual symbol end. Hence, we're trying to ensure
// that by scrolling to the symbol's end over than its start.
offsetToScroll++;
}
if (offsetToScroll >= 0 && offsetToScroll < myEditor.getDocument().getTextLength()) {
myEditor.getScrollingModel().scrollTo(
myEditor.offsetToLogicalPosition(offsetToScroll), ScrollType.RELATIVE
);
}
}
}
else {
markupModel.removeAllHighlighters();
}
myShowsPreviewHighlighters = !myShowsPreviewHighlighters;
}
protected Editor getEditor() {
return myEditor;
}
@NotNull
protected CodeStyleSettings getSettings() {
return mySettings;
}
public Set<String> processListOptions() {
return Collections.emptySet();
}
public final void applyPredefinedSettings(@NotNull PredefinedCodeStyle codeStyle) {
CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(ProjectUtil.guessCurrentProject(getPanel())).clone();
codeStyle.apply(settings);
reset(settings);
onSomethingChanged();
}
/**
* Override this method if the panel is linked to a specific language.
* @return The language this panel is associated with.
*/
@Nullable
public Language getDefaultLanguage() {
return myDefaultLanguage;
}
public void setLanguageSelector(LanguageSelector langSelector) {
if (myLanguageSelector == null) {
myLanguageSelector = langSelector;
}
}
public LanguageSelector getLanguageSelector() {
return myLanguageSelector;
}
protected String getTabTitle() {
return "Other";
}
public boolean setPanelLanguage(Language language) {
return false;
}
protected CodeStyleSettings getCurrentSettings() {
return myCurrentSettings;
}
public void setupCopyFromMenu(JPopupMenu copyMenu) {
copyMenu.removeAll();
}
public boolean isCopyFromMenuAvailable() {
return false;
}
}