| /* |
| * Copyright 2000-2011 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.codeInsight.editorActions; |
| |
| import com.intellij.codeInsight.template.TemplateManager; |
| import com.intellij.formatting.FormatConstants; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.formatter.WhiteSpaceFormattingStrategy; |
| import com.intellij.ide.DataManager; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.actionSystem.EditorActionManager; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.event.DocumentListener; |
| import com.intellij.openapi.editor.impl.TextChangeImpl; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.psi.codeStyle.CodeStyleSettings; |
| import com.intellij.psi.formatter.WhiteSpaceFormattingStrategyFactory; |
| import com.intellij.util.containers.WeakHashMap; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.util.Map; |
| |
| /** |
| * Encapsulates logic for processing {@link EditorSettings#isWrapWhenTypingReachesRightMargin(Project)} option. |
| * |
| * @author Denis Zhdanov |
| * @since 10/4/10 9:56 AM |
| */ |
| public class AutoHardWrapHandler { |
| |
| /** |
| * This key is used as a flag that indicates if <code>'auto wrap line on typing'</code> activity is performed now. |
| * |
| * @see CodeStyleSettings#WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN |
| */ |
| public static final Key<Boolean> AUTO_WRAP_LINE_IN_PROGRESS_KEY = new Key<Boolean>("AUTO_WRAP_LINE_IN_PROGRESS"); |
| |
| private static final AutoHardWrapHandler INSTANCE = new AutoHardWrapHandler(); |
| |
| /** |
| * There is a possible case that the user configured editor to |
| * {@link EditorSettings#isWrapWhenTypingReachesRightMargin(Project) wrap line on reaching right margin} and that he or she |
| * types in the middle of the long line. One line part is cut from end and moved to the next line as a result. But the user |
| * keeps typing and another part of the line should be moved to the next line then. We don't want to have a number of |
| * such line endings to be located on distinct lines. |
| * <p/> |
| * Hence, we remember last auto-wrap change per-document and merge it with the new auto-wrap if necessary. Current collection |
| * holds that <code>'document -> last auto-wrap change'</code> mappings. |
| */ |
| private final Map<Document, AutoWrapChange> myAutoWrapChanges = new WeakHashMap<Document, AutoWrapChange>(); |
| |
| public static AutoHardWrapHandler getInstance() { |
| return INSTANCE; |
| } |
| |
| /** |
| * The user is allowed to configured IJ in a way that it automatically wraps line on right margin exceeding on typing |
| * (check {@link EditorSettings#isWrapWhenTypingReachesRightMargin(Project)}). |
| * <p/> |
| * This method encapsulates that functionality, i.e. it performs the following logical actions: |
| * <pre> |
| * <ol> |
| * <li>Check if IJ is configured to perform automatic line wrapping on typing. Return in case of the negative answer;</li> |
| * <li>Check if right margin is exceeded. Return in case of the negative answer;</li> |
| * <li>Perform line wrapping;</li> |
| * </ol> |
| </pre> |
| * |
| * @param editor active editor |
| * @param dataContext current data context |
| * @param modificationStampBeforeTyping document modification stamp before the current symbols typing |
| */ |
| public void wrapLineIfNecessary(@NotNull Editor editor, @NotNull DataContext dataContext, long modificationStampBeforeTyping) { |
| Project project = editor.getProject(); |
| Document document = editor.getDocument(); |
| AutoWrapChange change = myAutoWrapChanges.get(document); |
| if (change != null) { |
| change.charTyped(editor, modificationStampBeforeTyping); |
| } |
| |
| // Return eagerly if we don't need to auto-wrap line, e.g. because of right margin exceeding. |
| if (/*editor.isOneLineMode() |
| || */project == null |
| || !editor.getSettings().isWrapWhenTypingReachesRightMargin(project) |
| || (TemplateManager.getInstance(project) != null && TemplateManager.getInstance(project).getActiveTemplate(editor) != null)) |
| { |
| return; |
| } |
| |
| CaretModel caretModel = editor.getCaretModel(); |
| int caretOffset = caretModel.getOffset(); |
| int line = document.getLineNumber(caretOffset); |
| int startOffset = document.getLineStartOffset(line); |
| int endOffset = document.getLineEndOffset(line); |
| |
| final CharSequence endOfString = document.getCharsSequence().subSequence(caretOffset, endOffset); |
| final boolean endsWithSpaces = StringUtil.isEmptyOrSpaces(String.valueOf(endOfString)); |
| // Check if right margin is exceeded. |
| int margin = editor.getSettings().getRightMargin(project); |
| if (margin <= 0) { |
| return; |
| } |
| |
| VisualPosition visEndLinePosition = editor.offsetToVisualPosition(endOffset); |
| if (margin >= visEndLinePosition.column) { |
| if (change != null) { |
| change.modificationStamp = document.getModificationStamp(); |
| } |
| return; |
| } |
| |
| // We assume that right margin is exceeded if control flow reaches this place. Hence, we define wrap position and perform |
| // smart line break there. |
| LineWrapPositionStrategy strategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(editor); |
| |
| // There is a possible case that user starts typing in the middle of the long string. Hence, there is a possible case that |
| // particular symbols were already wrapped because of typing. Example: |
| // a b c d e f g <caret>h i j k l m n o p| <- right margin |
| // Suppose the user starts typing at caret: |
| // type '1': a b c d e f g 1<caret>h i j k l m n o | <- right margin |
| // p | <- right margin |
| // type '2': a b c d e f g 12<caret>h i j k l m n o| <- right margin |
| // | <- right margin |
| // p | <- right margin |
| // type '3': a b c d e f g 123<caret>h i j k l m n | <- right margin |
| // o | <- right margin |
| // | <- right margin |
| // p | <- right margin |
| // We want to prevent such behavior, hence, we remove automatically generated wraps and wrap the line as a whole. |
| if (change == null) { |
| change = new AutoWrapChange(); |
| myAutoWrapChanges.put(document, change); |
| } |
| else { |
| final int start = change.change.getStart(); |
| final int end = change.change.getEnd(); |
| if (!change.isEmpty() && start < end) { |
| document.replaceString(start, end, change.change.getText()); |
| } |
| change.reset(); |
| } |
| change.update(editor); |
| |
| // Is assumed to be max possible number of characters inserted on the visual line with caret. |
| int maxPreferredOffset = editor.logicalPositionToOffset(editor.visualToLogicalPosition( |
| new VisualPosition(caretModel.getVisualPosition().line, margin - FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS) |
| )); |
| |
| int wrapOffset = strategy.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, true, false); |
| if (wrapOffset < 0) { |
| return; |
| } |
| |
| WhiteSpaceFormattingStrategy formattingStrategy = WhiteSpaceFormattingStrategyFactory.getStrategy(editor); |
| if (wrapOffset <= startOffset || wrapOffset > maxPreferredOffset |
| || formattingStrategy.check(document.getCharsSequence(), startOffset, wrapOffset) >= wrapOffset) |
| { |
| // Don't perform hard line wrapping if it doesn't makes sense (no point to wrap at first position and no point to wrap |
| // on first non-white space symbol because wrapped part will have the same indent value). |
| return; |
| } |
| |
| final int[] wrapIntroducedSymbolsNumber = new int[1]; |
| final int[] caretOffsetDiff = new int[1]; |
| final int baseCaretOffset = caretModel.getOffset(); |
| DocumentListener listener = new DocumentListener() { |
| @Override |
| public void beforeDocumentChange(DocumentEvent event) { |
| if (event.getOffset() < baseCaretOffset + caretOffsetDiff[0]) { |
| caretOffsetDiff[0] += event.getNewLength() - event.getOldLength(); |
| } |
| |
| if (autoFormatted(event)) { |
| return; |
| } |
| wrapIntroducedSymbolsNumber[0] += event.getNewLength() - event.getOldLength(); |
| } |
| |
| private boolean autoFormatted(DocumentEvent event) { |
| return event.getNewLength() <= event.getOldLength() && endsWithSpaces; |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| } |
| }; |
| |
| caretModel.moveToOffset(wrapOffset); |
| DataManager.getInstance().saveInDataContext(dataContext, AUTO_WRAP_LINE_IN_PROGRESS_KEY, true); |
| document.addDocumentListener(listener); |
| try { |
| EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(editor, dataContext); |
| } |
| finally { |
| DataManager.getInstance().saveInDataContext(dataContext, AUTO_WRAP_LINE_IN_PROGRESS_KEY, null); |
| document.removeDocumentListener(listener); |
| } |
| |
| change.modificationStamp = document.getModificationStamp(); |
| change.change.setStart(wrapOffset); |
| change.change.setEnd(wrapOffset + wrapIntroducedSymbolsNumber[0]); |
| |
| caretModel.moveToOffset(baseCaretOffset + caretOffsetDiff[0]); |
| } |
| |
| private static class AutoWrapChange { |
| |
| final TextChangeImpl change = new TextChangeImpl("", 0, 0); |
| int visualLine; |
| int logicalLine; |
| long modificationStamp; |
| |
| void reset() { |
| visualLine = -1; |
| logicalLine = -1; |
| change.setStart(0); |
| change.setEnd(0); |
| } |
| |
| void update(Editor editor) { |
| modificationStamp = editor.getDocument().getModificationStamp(); |
| |
| CaretModel caretModel = editor.getCaretModel(); |
| visualLine = caretModel.getVisualPosition().line; |
| logicalLine = caretModel.getLogicalPosition().line; |
| } |
| |
| void charTyped(Editor editor, long modificationStamp) { |
| if (matches(editor.getCaretModel(), modificationStamp)) { |
| this.modificationStamp = editor.getDocument().getModificationStamp(); |
| change.advance(1); |
| } |
| else { |
| reset(); |
| } |
| } |
| |
| boolean isEmpty() { |
| return change.getDiff() == 0; |
| } |
| |
| private boolean matches(CaretModel caretModel, long modificationStamp) { |
| return this.modificationStamp == modificationStamp && caretModel.getOffset() <= change.getStart() |
| && visualLine == caretModel.getVisualPosition().line && logicalLine == caretModel.getLogicalPosition().line; |
| } |
| |
| @Override |
| public String toString() { |
| return "visual line: " + visualLine + ", logical line: " + logicalLine + ", modification stamp: " + modificationStamp |
| + ", text change: " + change; |
| } |
| } |
| |
| } |