/* * 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.fileEditor.impl.text; import com.intellij.codeHighlighting.BackgroundEditorHighlighter; import com.intellij.ide.structureView.StructureViewBuilder; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; import com.intellij.psi.SingleRootFileViewProvider; import com.intellij.util.ui.update.UiNotifyConnector; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; /** * @author Anton Katilin * @author Vladimir Kondratyev */ public class TextEditorProvider implements FileEditorProvider, DumbAware { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.text.TextEditorProvider"); @TestOnly public static final Key<Boolean> TREAT_AS_SHOWN = Key.create("treat.editor.component.as.shown"); private static final Key<TextEditor> TEXT_EDITOR_KEY = Key.create("textEditor"); @NonNls private static final String TYPE_ID = "text-editor"; @NonNls private static final String LINE_ATTR = "line"; @NonNls private static final String COLUMN_ATTR = "column"; @NonNls private static final String LEAN_FORWARD_ATTR = "lean-forward"; @NonNls private static final String SELECTION_START_LINE_ATTR = "selection-start-line"; @NonNls private static final String SELECTION_START_COLUMN_ATTR = "selection-start-column"; @NonNls private static final String SELECTION_END_LINE_ATTR = "selection-end-line"; @NonNls private static final String SELECTION_END_COLUMN_ATTR = "selection-end-column"; @NonNls private static final String RELATIVE_CARET_POSITION_ATTR = "relative-caret-position"; @NonNls private static final String CARET_ELEMENT = "caret"; public static TextEditorProvider getInstance() { return ApplicationManager.getApplication().getComponent(TextEditorProvider.class); } @Override public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { return isTextFile(file) && !SingleRootFileViewProvider.isTooLargeForContentLoading(file); } @Override @NotNull public FileEditor createEditor(@NotNull Project project, @NotNull final VirtualFile file) { LOG.assertTrue(accept(project, file)); return new TextEditorImpl(project, file, this); } @Override @NotNull public FileEditorState readState(@NotNull Element element, @NotNull Project project, @NotNull VirtualFile file) { TextEditorState state = new TextEditorState(); try { List<Element> caretElements = element.getChildren(CARET_ELEMENT); if (caretElements.isEmpty()) { state.CARETS = new TextEditorState.CaretState[] {readCaretInfo(element)}; } else { state.CARETS = new TextEditorState.CaretState[caretElements.size()]; for (int i = 0; i < caretElements.size(); i++) { state.CARETS[i] = readCaretInfo(caretElements.get(i)); } } String verticalScrollProportion = element.getAttributeValue(RELATIVE_CARET_POSITION_ATTR); state.RELATIVE_CARET_POSITION = verticalScrollProportion == null ? 0 : Integer.parseInt(verticalScrollProportion); } catch (NumberFormatException ignored) { } return state; } private static TextEditorState.CaretState readCaretInfo(Element element) { TextEditorState.CaretState caretState = new TextEditorState.CaretState(); caretState.LINE = parseWithDefault(element, LINE_ATTR); caretState.COLUMN = parseWithDefault(element, COLUMN_ATTR); caretState.LEAN_FORWARD = parseBooleanWithDefault(element, LEAN_FORWARD_ATTR); caretState.SELECTION_START_LINE = parseWithDefault(element, SELECTION_START_LINE_ATTR); caretState.SELECTION_START_COLUMN = parseWithDefault(element, SELECTION_START_COLUMN_ATTR); caretState.SELECTION_END_LINE = parseWithDefault(element, SELECTION_END_LINE_ATTR); caretState.SELECTION_END_COLUMN = parseWithDefault(element, SELECTION_END_COLUMN_ATTR); return caretState; } private static int parseWithDefault(Element element, String attributeName) { String value = element.getAttributeValue(attributeName); return value == null ? 0 : Integer.parseInt(value); } private static boolean parseBooleanWithDefault(Element element, String attributeName) { String value = element.getAttributeValue(attributeName); return value != null && Boolean.parseBoolean(value); } @Override public void writeState(@NotNull FileEditorState _state, @NotNull Project project, @NotNull Element element) { TextEditorState state = (TextEditorState)_state; element.setAttribute(RELATIVE_CARET_POSITION_ATTR, Integer.toString(state.RELATIVE_CARET_POSITION)); if (state.CARETS != null) { for (TextEditorState.CaretState caretState : state.CARETS) { Element e = new Element(CARET_ELEMENT); e.setAttribute(LINE_ATTR, Integer.toString(caretState.LINE)); e.setAttribute(COLUMN_ATTR, Integer.toString(caretState.COLUMN)); e.setAttribute(LEAN_FORWARD_ATTR, Boolean.toString(caretState.LEAN_FORWARD)); e.setAttribute(SELECTION_START_LINE_ATTR, Integer.toString(caretState.SELECTION_START_LINE)); e.setAttribute(SELECTION_START_COLUMN_ATTR, Integer.toString(caretState.SELECTION_START_COLUMN)); e.setAttribute(SELECTION_END_LINE_ATTR, Integer.toString(caretState.SELECTION_END_LINE)); e.setAttribute(SELECTION_END_COLUMN_ATTR, Integer.toString(caretState.SELECTION_END_COLUMN)); element.addContent(e); } } } @Override @NotNull public String getEditorTypeId() { return TYPE_ID; } @Override @NotNull public FileEditorPolicy getPolicy() { return FileEditorPolicy.NONE; } @NotNull public TextEditor getTextEditor(@NotNull Editor editor) { TextEditor textEditor = editor.getUserData(TEXT_EDITOR_KEY); if (textEditor == null) { textEditor = createWrapperForEditor(editor); putTextEditor(editor, textEditor); } return textEditor; } @NotNull protected EditorWrapper createWrapperForEditor(@NotNull Editor editor) { return new EditorWrapper(editor); } @Nullable public static Document[] getDocuments(@NotNull FileEditor editor) { if (editor instanceof DocumentsEditor) { DocumentsEditor documentsEditor = (DocumentsEditor)editor; Document[] documents = documentsEditor.getDocuments(); return documents.length > 0 ? documents : null; } if (editor instanceof TextEditor) { Document document = ((TextEditor)editor).getEditor().getDocument(); return new Document[]{document}; } Project[] projects = ProjectManager.getInstance().getOpenProjects(); for (int i = projects.length - 1; i >= 0; i--) { VirtualFile file = FileEditorManagerEx.getInstanceEx(projects[i]).getFile(editor); if (file != null) { Document document = FileDocumentManager.getInstance().getDocument(file); if (document != null) { return new Document[]{document}; } } } return null; } static void putTextEditor(Editor editor, TextEditor textEditor) { editor.putUserData(TEXT_EDITOR_KEY, textEditor); } @NotNull protected TextEditorState getStateImpl(final Project project, @NotNull Editor editor, @NotNull FileEditorStateLevel level){ TextEditorState state = new TextEditorState(); CaretModel caretModel = editor.getCaretModel(); if (caretModel.supportsMultipleCarets()) { List<CaretState> caretsAndSelections = caretModel.getCaretsAndSelections(); state.CARETS = new TextEditorState.CaretState[caretsAndSelections.size()]; for (int i = 0; i < caretsAndSelections.size(); i++) { CaretState caretState = caretsAndSelections.get(i); LogicalPosition caretPosition = caretState.getCaretPosition(); LogicalPosition selectionStartPosition = caretState.getSelectionStart(); LogicalPosition selectionEndPosition = caretState.getSelectionEnd(); state.CARETS[i] = createCaretState(caretPosition, selectionStartPosition, selectionEndPosition); } } else { LogicalPosition caretPosition = caretModel.getLogicalPosition(); LogicalPosition selectionStartPosition = editor.offsetToLogicalPosition(editor.getSelectionModel().getSelectionStart()); LogicalPosition selectionEndPosition = editor.offsetToLogicalPosition(editor.getSelectionModel().getSelectionEnd()); state.CARETS = new TextEditorState.CaretState[1]; state.CARETS[0] = createCaretState(caretPosition, selectionStartPosition, selectionEndPosition); } // Saving scrolling proportion on UNDO may cause undesirable results of undo action fails to perform since // scrolling proportion restored slightly differs from what have been saved. state.RELATIVE_CARET_POSITION = level == FileEditorStateLevel.UNDO ? Integer.MAX_VALUE : EditorUtil.calcRelativeCaretPosition(editor); return state; } public static boolean isTextFile(@NotNull VirtualFile file) { if (file.isDirectory() || !file.isValid()) { return false; } final FileType ft = file.getFileType(); return !ft.isBinary() || BinaryFileTypeDecompilers.INSTANCE.forFileType(ft) != null; } private static TextEditorState.CaretState createCaretState(LogicalPosition caretPosition, LogicalPosition selectionStartPosition, LogicalPosition selectionEndPosition) { TextEditorState.CaretState caretState = new TextEditorState.CaretState(); caretState.LINE = getLine(caretPosition); caretState.COLUMN = getColumn(caretPosition); caretState.LEAN_FORWARD = caretPosition != null && caretPosition.leansForward; caretState.SELECTION_START_LINE = getLine(selectionStartPosition); caretState.SELECTION_START_COLUMN = getColumn(selectionStartPosition); caretState.SELECTION_END_LINE = getLine(selectionEndPosition); caretState.SELECTION_END_COLUMN = getColumn(selectionEndPosition); return caretState; } private static int getLine(@Nullable LogicalPosition pos) { return pos == null ? 0 : pos.line; } private static int getColumn(@Nullable LogicalPosition pos) { return pos == null ? 0 : pos.column; } protected void setStateImpl(final Project project, final Editor editor, final TextEditorState state){ if (state.CARETS != null && state.CARETS.length > 0) { if (editor.getCaretModel().supportsMultipleCarets()) { CaretModel caretModel = editor.getCaretModel(); List<CaretState> states = new ArrayList<>(state.CARETS.length); for (TextEditorState.CaretState caretState : state.CARETS) { states.add(new CaretState(new LogicalPosition(caretState.LINE, caretState.COLUMN, caretState.LEAN_FORWARD), new LogicalPosition(caretState.SELECTION_START_LINE, caretState.SELECTION_START_COLUMN), new LogicalPosition(caretState.SELECTION_END_LINE, caretState.SELECTION_END_COLUMN))); } caretModel.setCaretsAndSelections(states, false); } else { TextEditorState.CaretState caretState = state.CARETS[0]; LogicalPosition pos = new LogicalPosition(caretState.LINE, caretState.COLUMN); editor.getCaretModel().moveToLogicalPosition(pos); int startOffset = editor.logicalPositionToOffset(new LogicalPosition(caretState.SELECTION_START_LINE, caretState.SELECTION_START_COLUMN)); int endOffset = editor.logicalPositionToOffset(new LogicalPosition(caretState.SELECTION_END_LINE, caretState.SELECTION_END_COLUMN)); if (startOffset == endOffset) { editor.getSelectionModel().removeSelection(); } else { editor.getSelectionModel().setSelection(startOffset, endOffset); } } } final int relativeCaretPosition = state.RELATIVE_CARET_POSITION; Runnable scrollingRunnable = () -> { if (!editor.isDisposed()) { editor.getScrollingModel().disableAnimation(); if (relativeCaretPosition != Integer.MAX_VALUE) { EditorUtil.setRelativeCaretPosition(editor, relativeCaretPosition); } editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); editor.getScrollingModel().enableAnimation(); } }; //noinspection TestOnlyProblems if (Boolean.TRUE.equals(editor.getUserData(TREAT_AS_SHOWN))) scrollingRunnable.run(); else UiNotifyConnector.doWhenFirstShown(editor.getContentComponent(), scrollingRunnable); } protected class EditorWrapper extends UserDataHolderBase implements TextEditor { private final Editor myEditor; EditorWrapper(@NotNull Editor editor) { myEditor = editor; } @Override @NotNull public Editor getEditor() { return myEditor; } @Override @NotNull public JComponent getComponent() { return myEditor.getComponent(); } @Override public JComponent getPreferredFocusedComponent() { return myEditor.getContentComponent(); } @Override @NotNull public String getName() { return "Text"; } @Override public StructureViewBuilder getStructureViewBuilder() { VirtualFile file = FileDocumentManager.getInstance().getFile(myEditor.getDocument()); if (file == null) return null; final Project project = myEditor.getProject(); LOG.assertTrue(project != null); return StructureViewBuilder.PROVIDER.getStructureViewBuilder(file.getFileType(), file, project); } @Override @NotNull public FileEditorState getState(@NotNull FileEditorStateLevel level) { return getStateImpl(null, myEditor, level); } @Override public void setState(@NotNull FileEditorState state) { setStateImpl(null, myEditor, (TextEditorState)state); } @Override public boolean isModified() { return false; } @Override public boolean isValid() { return true; } @Override public void dispose() { } @Override public void selectNotify() { } @Override public void deselectNotify() { } @Override public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { } @Override public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { } @Override public BackgroundEditorHighlighter getBackgroundHighlighter() { return null; } @Override public FileEditorLocation getCurrentLocation() { return null; } @Override public boolean canNavigateTo(@NotNull final Navigatable navigatable) { return false; } @Override public void navigateTo(@NotNull final Navigatable navigatable) { } } }