All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.editor.BaseDocument Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.editor;

import java.awt.Font;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeSupport;
import java.beans.VetoableChangeListener;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Hashtable;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
import javax.swing.JEditorPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import javax.swing.text.EditorKit;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.StyleConstants;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.WrappedPlainView;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
import org.netbeans.api.editor.document.CustomUndoDocument;
import org.netbeans.api.editor.document.LineDocument;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.SimpleValueNames;
import org.netbeans.lib.editor.util.CharSequenceUtilities;
import org.netbeans.lib.editor.util.ListenerList;
import org.netbeans.lib.editor.util.swing.DocumentListenerPriority;
import org.netbeans.modules.editor.document.implspi.CharClassifier;
import org.netbeans.modules.editor.indent.api.Reformat;
import org.netbeans.modules.editor.lib.BaseDocument_PropertyHandler;
import org.netbeans.modules.editor.lib.BeforeSaveTasks;
import org.netbeans.modules.editor.lib.EditorPackageAccessor;
import org.netbeans.modules.editor.lib.SettingsConversions;
import org.netbeans.modules.editor.lib.WcwdithUtil;
import org.netbeans.modules.editor.lib.drawing.DrawEngine;
import org.netbeans.modules.editor.lib.drawing.DrawGraphics;
import org.netbeans.modules.editor.lib.impl.MarkVector;
import org.netbeans.modules.editor.lib.impl.MultiMark;
import org.netbeans.modules.editor.lib2.CaretUndo;
import org.netbeans.modules.editor.lib2.EditorPreferencesDefaults;
import org.netbeans.modules.editor.lib2.EditorPreferencesKeys;
import org.netbeans.modules.editor.lib2.document.ContentEdit;
import org.netbeans.modules.editor.lib2.document.EditorDocumentContent;
import org.netbeans.modules.editor.lib2.document.EditorDocumentHandler;
import org.netbeans.modules.editor.lib2.document.EditorDocumentServices;
import org.netbeans.modules.editor.lib2.document.LineRootElement;
import org.netbeans.modules.editor.lib2.document.ListUndoableEdit;
import org.netbeans.modules.editor.lib2.document.ModRootElement;
import org.netbeans.modules.editor.lib2.document.DocumentPostModificationUtils;
import org.netbeans.modules.editor.lib2.document.ReadWriteBuffer;
import org.netbeans.modules.editor.lib2.document.ReadWriteUtils;
import org.netbeans.modules.editor.lib2.document.StableCompoundEdit;
import org.netbeans.modules.editor.lib2.document.UndoRedoDocumentEventResolver;
import org.netbeans.spi.editor.document.UndoableEditWrapper;
import org.netbeans.spi.lexer.MutableTextInput;
import org.netbeans.spi.lexer.TokenHierarchyControl;
import org.openide.filesystems.FileObject;
import org.openide.util.WeakListeners;

/**
* Document implementation
*
* @author Miloslav Metelka
* @version 1.00
*/

@SuppressWarnings("ClassWithMultipleLoggers")
public class BaseDocument extends AbstractDocument implements AtomicLockDocument, LineDocument, CustomUndoDocument {

    static {
        EditorPackageAccessor.register(new Accessor());
        EditorDocumentHandler.setEditorDocumentServices(BaseDocument.class, BaseDocumentServices.INSTANCE);
        UndoRedoDocumentEventResolver.register(new UndoRedoDocumentEventResolver() {
            @Override
            public boolean isUndoRedo(DocumentEvent evt) {
                if (evt instanceof BaseDocumentEvent) {
                    BaseDocumentEvent bevt = (BaseDocumentEvent) evt;
                    return bevt.isInUndo() || bevt.isInRedo();
                }
                return false;
            }
        });
    }

    // -J-Dorg.netbeans.editor.BaseDocument.level=FINE
    private static final Logger LOG = Logger.getLogger(BaseDocument.class.getName());

    // -J-Dorg.netbeans.editor.BaseDocument-listener.level=FINE
    private static final Logger LOG_LISTENER = Logger.getLogger(BaseDocument.class.getName() + "-listener");

    // -J-Dorg.netbeans.editor.BaseDocument.EDT.level=FINE - check that insert/remove only in EDT
//    private static final Logger LOG_EDT = Logger.getLogger(BaseDocument.class.getName() + "-EDT");
    
    /**
     * Mime type of the document. This property can be used for determining
     * mime type of a document.
     *
     * @since 1.26
     */
    public static final String MIME_TYPE_PROP = "mimeType"; // NOI18N

    /** Registry identification property */
    public static final String ID_PROP = "id"; // NOI18N

    /** This document's version. It's accessed by DocumentUtilities.getDocumentVersion(). */
    private static final String VERSION_PROP = "version"; //NOI18N

    /** Timestamp when this document was last modified. It's accessed by DocumentUtilities.getDocumentVersion(). */
    private static final String LAST_MODIFICATION_TIMESTAMP_PROP = "last-modification-timestamp"; //NOI18N

    /** Line separator property for reading files in */
    public static final String READ_LINE_SEPARATOR_PROP = DefaultEditorKit.EndOfLineStringProperty;

    /** Line separator property for writing content into files. If not set
     * the writing defaults to the READ_LINE_SEPARATOR_PROP.
     */
    public static final String WRITE_LINE_SEPARATOR_PROP = "write-line-separator"; // NOI18N

    /** File name property */
    public static final String FILE_NAME_PROP = "file-name"; // NOI18N

    /** Wrap search mark property */
    public static final String WRAP_SEARCH_MARK_PROP = "wrap-search-mark"; // NOI18N

    /** Undo manager property. This can be used to implement undo
    * in a simple way. Default undo and redo actions try to get this
    * property and perform undo and redo through it.
    */
    public static final String UNDO_MANAGER_PROP = "undo-manager"; // NOI18N

    /** Kit class property. This can become useful for getting
    * the settings that logicaly belonging to the document.
    */
    public static final String KIT_CLASS_PROP = "kit-class"; // NOI18N

    /** String forward finder property */
    public static final String STRING_FINDER_PROP = "string-finder"; // NOI18N

    /** String backward finder property */
    public static final String STRING_BWD_FINDER_PROP = "string-bwd-finder"; // NOI18N

    /** Highlight search finder property. */
    public static final String BLOCKS_FINDER_PROP = "blocks-finder"; // NOI18N

    /**
     * Maximum line width encountered during the initial read operation.
     * This is filled by Analyzer and used by UI to set the correct initial width
     * of the component.
     * Values: java.lang.Integer
     * @deprecated property no longer populated; deprecated without replacement.
     */
    public static final String LINE_LIMIT_PROP = "line-limit"; // NOI18N

    /**
     * If set, determines the document's editability. Note that even though the 
     * editable property may be set to true, the document may be still uneditable for
     * other reasons. The document should not permit any edits if the editable
     * property is set to false.
     */
    /* public */ static final String EDITABLE_PROP = "editable"; // NOI18N

    /**
     * Size of the line batch. Line batch can be used at various places
     * especially when processing lines by syntax scanner.
     * @deprecated property no longer populated; deprecated without replacement.
     */
    public static final String LINE_BATCH_SIZE = "line-batch-size"; // NOI18N

    /** Line separator is marked by CR (Macintosh) */
    public static final String  LS_CR = "\r"; // NOI18N

    /** Line separator is marked by LF (Unix) */
    public static final String  LS_LF = "\n"; // NOI18N

    /** Line separator is marked by CR and LF (Windows) */
    public static final String  LS_CRLF = "\r\n"; // NOI18N

    /** Name of the formatter setting. */
    public static final String FORMATTER = "formatter"; // NOI18N

    /**
     * Document Boolean property defined by openide.text.CloneableEditorSupport to determine
     * whether document implementation fires VetoableChangeListener prior doing actual write-locking
     * followed by document modification or an atomic section.
     */
    private static final String SUPPORTS_MODIFICATION_LISTENER_PROP = "supportsModificationListener"; // NOI18N

    /**
     * Document property into which openide.text.CloneableEditorSupport
     * puts VetoableChangeListener instance that the document fires prior doing actual write-locking
     * followed by document modification or an atomic section.
     */
    private static final String MODIFICATION_LISTENER_PROP = "modificationListener"; // NOI18N
    /**
     * How many modifications under atomic lock are necessary for disabling of lexer's token hierarchy.
     */
    private static final int DEACTIVATE_LEXER_THRESHOLD = 30;

    private static final Object annotationsLock = new Object();

    /** Debug the stack of calling of the insert/remove */
    private static final boolean debugStack = Boolean.getBoolean("netbeans.debug.editor.document.stack"); // NOI18N
    /** Debug the document insert/remove but do not output text inserted/removed */
    private static final boolean debugNoText = Boolean.getBoolean("netbeans.debug.editor.document.notext"); // NOI18N

    /** Debug the StreamDescriptionProperty during read() */
    private static final boolean debugRead = Boolean.getBoolean("netbeans.debug.editor.document.read"); // NOI18N

    /** How many times atomic writer requested writing */
    private int atomicDepth;
    
    /**
     * If VetoableChangeListener for "modificationListener" property fired
     * veto which means that any document's modification is prohibited then this variable will become false.
     * Each insertString() or remove() will generate BadLocationException.
     * If no veto is fired or no listener is present then this variable is true.
     */
    boolean modifiable = true;

    /* Was the document initialized by reading? */
    protected boolean inited;

    /* Was the document modified by doing inert/remove */
    protected boolean modified;

    /** Default element - lazily inited */
    protected Element defaultRootElem;

    private SyntaxSupport syntaxSupport;

    /** Reset merging next created undoable edit to the last one. */
    boolean undoMergeReset;

    /** Kit class stored here */
    private final Class deprecatedKitClass;
    private String mimeType;

    /** Undo event for atomic events is fired after the successful
    * atomic operation is finished. The changes are stored in this variable
    * during the atomic operation. If the operation is broken, these edits
    * are used to restore previous state.
    */
    private AtomicCompoundEdit atomicEdits;

    private Acceptor identifierAcceptor;

    private Acceptor whitespaceAcceptor;

//    private final ArrayList syntaxList = new ArrayList();

    /** Root element of line elements representation */
    LineRootElement lineRootElement;

    /** Last document event to be undone. The field is filled
     * by the lastly done modification undoable edit.
     * BaseDocumentEvent.canUndo() checks this flag.
     */
    UndoableEdit lastModifyUndoEdit; // #8692 check last modify undo edit

    /** List of annotations for this document. */
    private Annotations annotations;

    /* Bug #6258 during composing I18N text using input method the Undoable Edit
     * actions must be disabled so only the final push (and not all intermediate
     * ones) of the I18N word will be stored as undoable edit.
     */
     private boolean composedText = false;

    /** Atomic lock event instance shared by all the atomic lock firings done for this document */
    private AtomicLockEvent atomicLockEventInstance = new AtomicLockEvent(this);

    private FixLineSyntaxState fixLineSyntaxState;

    private Object[] atomicLockListenerList;
    
    private int postModificationDepth;

    private DocumentListener postModificationDocumentListener;

    private ListenerList postModificationDocumentListenerList = new ListenerList();

    private ListenerList updateDocumentListenerList = new ListenerList();

    private Position lastPositionEditedByTyping = null;

    /** Size of one indentation level. If this variable is null (value
     * is not set in Settings, then the default algorithm will be used.
     */
    private int shiftWidth = -1;
    private int tabSize;
    
    private CharSequence text;
    
    private UndoableEdit removeUpdateLineUndo;

    private Collection undoEditWrappers;

    private DocumentFilter.FilterBypass filterBypass;
    
    private int runExclusiveDepth;

    private Preferences prefs;
    private final PreferenceChangeListener prefsListener = new PreferenceChangeListener() {
        public @Override void preferenceChange(PreferenceChangeEvent evt) {
            String key = evt == null ? null : evt.getKey();
            if (key == null || SimpleValueNames.TAB_SIZE.equals(key)) {
                tabSize = prefs.getInt(SimpleValueNames.TAB_SIZE, EditorPreferencesDefaults.defaultTabSize);
            }

            if (key == null || SimpleValueNames.INDENT_SHIFT_WIDTH.equals(key)) {
                shiftWidth = prefs.getInt(SimpleValueNames.INDENT_SHIFT_WIDTH, -1);
            }

            if (key == null || SimpleValueNames.SPACES_PER_TAB.equals(key)) {
                if (shiftWidth == -1) {
                    shiftWidth = prefs.getInt(SimpleValueNames.SPACES_PER_TAB, EditorPreferencesDefaults.defaultSpacesPerTab);
                }
            }

            if (key == null || EditorPreferencesKeys.READ_BUFFER_SIZE.equals(key)) {
                int readBufferSize = prefs.getInt(EditorPreferencesKeys.READ_BUFFER_SIZE, -1);
                if (readBufferSize <= 0) {
                    readBufferSize = EditorPreferencesDefaults.defaultReadBufferSize;
                }
                putProperty(EditorPreferencesKeys.READ_BUFFER_SIZE, Integer.valueOf(readBufferSize));
            }

            if (key == null || EditorPreferencesKeys.WRITE_BUFFER_SIZE.equals(key)) {
                int writeBufferSize = prefs.getInt(EditorPreferencesKeys.WRITE_BUFFER_SIZE, -1);
                if (writeBufferSize <= 0) {
                    writeBufferSize = EditorPreferencesDefaults.defaultWriteBufferSize;
                }
                putProperty(EditorPreferencesKeys.WRITE_BUFFER_SIZE, Integer.valueOf(writeBufferSize));
            }

            if (key == null || EditorPreferencesKeys.MARK_DISTANCE.equals(key)) {
                int markDistance = prefs.getInt(EditorPreferencesKeys.MARK_DISTANCE, -1);
                if (markDistance <= 0) {
                    markDistance = EditorPreferencesDefaults.defaultMarkDistance;
                }
                putProperty(EditorPreferencesKeys.MARK_DISTANCE, Integer.valueOf(markDistance));
            }

            if (key == null || EditorPreferencesKeys.MAX_MARK_DISTANCE.equals(key)) {
                int maxMarkDistance = prefs.getInt(EditorPreferencesKeys.MAX_MARK_DISTANCE, -1);
                if (maxMarkDistance <= 0) {
                    maxMarkDistance = EditorPreferencesDefaults.defaultMaxMarkDistance;
                }
                putProperty(EditorPreferencesKeys.MAX_MARK_DISTANCE, Integer.valueOf(maxMarkDistance));
            }

            if (key == null || EditorPreferencesKeys.MIN_MARK_DISTANCE.equals(key)) {
                int minMarkDistance = prefs.getInt(EditorPreferencesKeys.MIN_MARK_DISTANCE, -1);
                if (minMarkDistance <=0 ) {
                    minMarkDistance = EditorPreferencesDefaults.defaultMinMarkDistance;
                }
                putProperty(EditorPreferencesKeys.MIN_MARK_DISTANCE, Integer.valueOf(minMarkDistance));
            }

            if (key == null || EditorPreferencesKeys.READ_MARK_DISTANCE.equals(key)) {
                int readMarkDistance = prefs.getInt(EditorPreferencesKeys.READ_MARK_DISTANCE, -1);
                if (readMarkDistance <= 0) {
                    readMarkDistance = EditorPreferencesDefaults.defaultReadMarkDistance;
                }
                putProperty(EditorPreferencesKeys.READ_MARK_DISTANCE, Integer.valueOf(readMarkDistance));
            }

            if (key == null || EditorPreferencesKeys.SYNTAX_UPDATE_BATCH_SIZE.equals(key)) {
                int syntaxUpdateBatchSize = prefs.getInt(EditorPreferencesKeys.SYNTAX_UPDATE_BATCH_SIZE, -1);
                if (syntaxUpdateBatchSize <= 0) {
                    syntaxUpdateBatchSize = 7 * (Integer) getProperty(EditorPreferencesKeys.MARK_DISTANCE);
                }
                putProperty(EditorPreferencesKeys.SYNTAX_UPDATE_BATCH_SIZE, Integer.valueOf(syntaxUpdateBatchSize));
            }

            if (key == null || LINE_BATCH_SIZE.equals(key)) {
                int lineBatchSize = prefs.getInt(LINE_BATCH_SIZE, -1);
                if (lineBatchSize <= 0) {
                    lineBatchSize = EditorPreferencesDefaults.defaultLineBatchSize;
                }
                putProperty(LINE_BATCH_SIZE, Integer.valueOf(lineBatchSize));
            }

            if (key == null || EditorPreferencesKeys.IDENTIFIER_ACCEPTOR.equals(key)) {
                identifierAcceptor = (Acceptor) SettingsConversions.callFactory(prefs, MimePath.parse(mimeType), EditorPreferencesKeys.IDENTIFIER_ACCEPTOR, AcceptorFactory.LETTER_DIGIT);
            }

            if (key == null || EditorPreferencesKeys.WHITESPACE_ACCEPTOR.equals(key)) {
                whitespaceAcceptor = (Acceptor) SettingsConversions.callFactory(prefs, MimePath.parse(mimeType), EditorPreferencesKeys.WHITESPACE_ACCEPTOR, AcceptorFactory.WHITESPACE);
            }

            boolean stopOnEOL = prefs.getBoolean(EditorPreferencesKeys.WORD_MOVE_NEWLINE_STOP, EditorPreferencesDefaults.defaultWordMoveNewlineStop);

            if (key == null || EditorPreferencesKeys.NEXT_WORD_FINDER.equals(key)) {
                Finder finder = (Finder) SettingsConversions.callFactory(prefs, MimePath.parse(mimeType), EditorPreferencesKeys.NEXT_WORD_FINDER, null);
                putProperty(EditorPreferencesKeys.NEXT_WORD_FINDER, finder != null ? finder : new FinderFactory.NextWordFwdFinder(BaseDocument.this, stopOnEOL, false));
            }

            if (key == null || EditorPreferencesKeys.PREVIOUS_WORD_FINDER.equals(key)) {
                Finder finder = (Finder) SettingsConversions.callFactory(prefs, MimePath.parse(mimeType), EditorPreferencesKeys.PREVIOUS_WORD_FINDER, null);
                putProperty(EditorPreferencesKeys.PREVIOUS_WORD_FINDER, finder != null ? finder : new FinderFactory.PreviousWordBwdFinder(BaseDocument.this, stopOnEOL, false));
            }

            SettingsConversions.callSettingsChange(BaseDocument.this);
        }
    };
    private PreferenceChangeListener weakPrefsListener;

    /**
     * Creates a new document.
     *
     * @param kitClass class used to initialize this document with proper settings
     *   category based on the editor kit for which this document is created
     * @param addToRegistry If true the document will be listed in the
     *   EditorRegistry. In most situations true is the
     *   correct. However, if you are absolutely sure that document should not be
     *   listed in the registry, then set this parameter to false.
     *
     * @deprecated Use of editor kit's implementation classes is deprecated
     *   in favor of mime types.
     */
    public BaseDocument(Class kitClass, boolean addToRegistry) {
        super(new EditorDocumentContent());

        if (LOG.isLoggable(Level.FINE) || LOG.isLoggable(Level.WARNING)) {
            String msg = "Using deprecated document construction for " + //NOI18N
                getClass().getName() + ", " + //NOI18N
                "see http://www.netbeans.org/nonav/issues/show_bug.cgi?id=114747. " + //NOI18N
                "Use -J-Dorg.netbeans.editor.BaseDocument.level=500 to see the stacktrace."; //NOI18N

            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE, null, new Throwable(msg)); //NOI18N
            } else {
                LOG.warning(msg); //NOI18N
            }
        }

        this.deprecatedKitClass = kitClass;
        this.mimeType = BaseKit.getKit(kitClass).getContentType();
        init(addToRegistry);
    }

    /**
     * Creates a new document.
     *
     * @param addToRegistry If true the document will be listed in the
     *   EditorRegistry. In most situations true is the
     *   correct. However, if you are absolutely sure that document should not be
     *   listed in the registry, then set this parameter to false.
     * @param mimeType The mime type for the document.
     *
     * @since 1.26
     */
    public BaseDocument(boolean addToRegistry, String mimeType) {
        super(new EditorDocumentContent());
        this.deprecatedKitClass = null;
        this.mimeType = mimeType;
        init(addToRegistry);
    }

    private void init(boolean addToRegistry) {
//        System.out.println("~~~ " + s2s(this) + " created for '" + mimeType + "'");
//
        setDocumentProperties(createDocumentProperties(getDocumentProperties()));
        super.addDocumentListener(org.netbeans.lib.editor.util.swing.DocumentUtilities.initPriorityListening(this));

        text = ((EditorDocumentContent)getContent()).getText();
        putProperty(CharSequence.class, text);
        putProperty(GapStart.class, new GapStart() {
            @Override
            public int getGapStart() {
                return ((EditorDocumentContent)getContent()).getCharContentGapStart();
            }
        });
        putProperty(SUPPORTS_MODIFICATION_LISTENER_PROP, Boolean.TRUE); // NOI18N
        putProperty(MIME_TYPE_PROP, new MimeTypePropertyEvaluator(this));
        putProperty(VERSION_PROP, new AtomicLong());
        putProperty(LAST_MODIFICATION_TIMESTAMP_PROP, new AtomicLong());
        putProperty(SimpleValueNames.TAB_SIZE, new BaseDocument_PropertyHandler() {
            public @Override Object setValue(Object value) {
                return null;
            }

            public @Override Object getValue() {
                return getTabSize();
            }
        });
        putProperty(PropertyChangeSupport.class, new PropertyChangeSupport(this));

        lineRootElement = new LineRootElement(this);

        // Line separators default to platform ones
        putProperty(READ_LINE_SEPARATOR_PROP, ReadWriteUtils.getSystemLineSeparator());

        // Initialize preferences and document properties
        prefs = MimeLookup.getLookup(mimeType).lookup(Preferences.class);
        prefsListener.preferenceChange(null);
//        System.out.println("~~~ init: '" + mimeType + "' -> " + s2s(prefs));

        // Additional initialization of the document through the kit
        EditorKit kit = getEditorKit();
        if (kit instanceof BaseKit) {
            ((BaseKit) kit).initDocument(this);
        }

        ModRootElement modElementRoot = new ModRootElement(this);
        this.addUpdateDocumentListener(modElementRoot);
        modElementRoot.setEnabled(true);

        BeforeSaveTasks.get(this); // Ensure that "beforeSaveRunnable" gets initialized

        undoEditWrappers = MimeLookup.getLookup(mimeType).lookupAll(UndoableEditWrapper.class);
        if (undoEditWrappers != null && undoEditWrappers.isEmpty()) {
            undoEditWrappers = null;
        }

        if (weakPrefsListener == null) {
            // the listening could have already been initialized from setMimeType(), which
            // is called by some kits from initDocument()
            weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs);
            prefs.addPreferenceChangeListener(weakPrefsListener);
//            System.out.println("~~~ init: " + s2s(prefs) + " adding " + s2s(weakPrefsListener));
        }
    }
    
    public CharSeq getText() {
        return new CharSeq() {
            @Override
            public int length() {
                return text.length();
            }
            @Override
            public char charAt(int index) {
                return text.charAt(index);
            }
        };
    }

    Syntax getFreeSyntax() {
        EditorKit kit = getEditorKit();
        if (kit instanceof BaseKit) {
            return ((BaseKit) kit).createSyntax(this);
        } else {
            return new BaseKit.DefaultSyntax();
        }
//        synchronized (syntaxList) {
//            int cnt = syntaxList.size();
//            if (cnt > 0) {
//                return syntaxList.remove(cnt - 1);
//            } else {
//                EditorKit kit = getEditorKit();
//                if (kit instanceof BaseKit) {
//                    return ((BaseKit) kit).createSyntax(this);
//                } else {
//                    return new BaseKit.DefaultSyntax();
//                }
//            }
//        }
    }

    void releaseSyntax(Syntax syntax) {
//        synchronized (syntaxList) {
//            syntaxList.add(syntax);
//        }
    }

// XXX: formatting cleanup
//    /**
//     * @deprecated Please use Editor Indentation API instead, for details see
//     *   Editor Indentation.
//     */
//    public Formatter getLegacyFormatter() {
//        if (formatter == null) {
//            formatter = (Formatter) SettingsConversions.callFactory(prefs, MimePath.parse(mimeType), FORMATTER, null);
//            if (formatter == null) {
//                formatter = Formatter.getFormatter(mimeType);
//            }
//        }
//        return formatter;
//    }
//
//    /**
//     * Gets the formatter for this document.
//     *
//     * @deprecated Please use Editor Indentation API instead, for details see
//     *   Editor Indentation.
//     */
//    public Formatter getFormatter() {
//        Formatter f = getLegacyFormatter();
//        FormatterOverride fp = Lookup.getDefault().lookup(FormatterOverride.class);
//        return (fp != null) ? fp.getFormatter(this, f) : f;
//    }

    /**
     * @deprecated Please use Lexer instead, for details see
     *   Lexer.
     */
    public SyntaxSupport getSyntaxSupport() {
        if (syntaxSupport == null) {
            EditorKit kit = getEditorKit();
            if (kit instanceof BaseKit) {
                syntaxSupport = ((BaseKit) kit).createSyntaxSupport(this);
            } else {
                syntaxSupport = new SyntaxSupport(this);
            }
        }
        return syntaxSupport;
    }

    /** Perform any generic text processing. The advantage of this method
    * is that it allows the text to processed in line batches. The initial
    * size of the batch is given by the SettingsNames.LINE_BATCH_SIZE.
    * The TextBatchProcessor.processTextBatch() method is called for every
    * text batch. If the method returns true, it means the processing should
    * continue with the next batch of text which will have double line count
    * compared to the previous one. This guarantees there will be not too many
    * batches so the processing should be more efficient.
    * @param tbp text batch processor to be used to process the text batches
    * @param startPos starting position of the processing.
    * @param endPos ending position of the processing. This can be -1 to signal
    *   the end of document. If the endPos is lower than startPos then the batches
    *   are created in the backward direction.
    * @return the returned value from the last tpb.processTextBatch() call.
    *   The -1 will be returned for (startPos == endPos).
    */
    public int processText(TextBatchProcessor tbp, int startPos, int endPos)
    throws BadLocationException {
        if (endPos == -1) {
            endPos = getLength();
        }
        int batchLineCnt = ((Integer)getProperty(LINE_BATCH_SIZE)).intValue();
        int batchStart = startPos;
        int ret = -1;
        if (startPos < endPos) { // batching in forward direction
            while (ret < 0 && batchStart < endPos) {
                int batchEnd = Math.min(Utilities.getRowStart(this, batchStart, batchLineCnt), endPos);
                if (batchEnd == -1) { // getRowStart() returned -1
                    batchEnd = endPos;
                }
                ret = tbp.processTextBatch(this, batchStart, batchEnd, (batchEnd == endPos));
                batchLineCnt *= 2; // double the scanned area
                batchStart = batchEnd;
            }
        } else {
            while (ret < 0 && batchStart > endPos) {
                int batchEnd = Math.max(Utilities.getRowStart(this, batchStart, -batchLineCnt), endPos);
                ret = tbp.processTextBatch(this, batchStart, batchEnd, (batchEnd == endPos));
                batchLineCnt *= 2; // double the scanned area
                batchStart = batchEnd;
            }
        }
        return ret;
    }


    public boolean isIdentifierPart(char ch) {
        return identifierAcceptor.accept(ch);
    }

    public boolean isWhitespace(char ch) {
        return whitespaceAcceptor.accept(ch);
    }
    
    /**
     * When called within runnable of {@link #runAtomic(java.lang.Runnable) }
     * this method returns true if the document can be mutated or false
     * if any attempt of inserting/removing text would throw {@link GuardedException}.
     *
     * @return true if document can be mutated by
     * {@link #insertString(int, java.lang.String, javax.swing.text.AttributeSet)}
     *  or {@link #remove(int, int) }.
     *
     * @since 3.17
     */
    public boolean isModifiable() {
        return modifiable;
    }
    
    /** Inserts string into document */
    public @Override void insertString(int offset, String text, AttributeSet attrs)
    throws BadLocationException {
//        if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT
//            // Disabled due to failing OpenEditorEnablesEditMenuFactoryTest
//            if (!SwingUtilities.isEventDispatchThread()) {
//                throw new IllegalStateException("BaseDocument.insertString not in EDT: offset=" + // NOI18N
//                        offset + ", text=" + org.netbeans.lib.editor.util.CharSequenceUtilities.debugText(text)); // NOI18N
//            }
//        }
        
        // Always acquire atomic lock (it simplifies processing and improves readability)
        atomicLockImpl();
        try {
            checkModifiable(offset);
            DocumentFilter filter = getDocumentFilter();
            if (filter != null) {
                filter.insertString(getFilterBypass(), offset, text, attrs);
            } else {
                handleInsertString(offset, text, attrs);
            }
        } finally {
            atomicUnlockImpl(true);
        }
    }
    
    void handleInsertString(int offset, String text, AttributeSet attrs) throws BadLocationException {
        if (text == null || text.length() == 0) {
            return;
        }

        // Check offset correctness
        if (offset < 0 || offset > getLength()) {
            throw new BadLocationException("Wrong insert position " + offset, offset); // NOI18N
        }

        // possible CR-LF to LF conversion
        text = ReadWriteUtils.convertToNewlines(text);

        incrementDocVersion();

        preInsertCheck(offset, text, attrs);

        if (LOG.isLoggable(Level.FINE)) {
            StringBuilder sb = new StringBuilder(200);
            sb.append("insertString(): doc="); // NOI18N
            appendInfoTerse(sb);
            sb.append(modified ? "" : " - first modification"). // NOI18N
                    append(", offset=").append(Utilities.offsetToLineColumnString(this, offset)); // NOI18N
            if (!debugNoText) {
                sb.append(" \"");
                appendContext(sb, offset);
                sb.append("\" + \""); // NOI18N
                if (modified) {
                    CharSequenceUtilities.debugText(sb, text);
                } else { // For first modification display regular text for easier orientation
                    sb.append(text);
                }
                sb.append("\""); // NOI18N
            }

            if (debugStack) {
                LOG.log(Level.FINE, sb.toString(), new Throwable("Insert stack"));  // NOI18N
            } else {
                LOG.log(Level.FINE, sb.toString());
            }
        }

        UndoableEdit edit = getContent().insertString(offset, text);

        BaseDocumentEvent evt = getDocumentEvent(offset, text.length(), DocumentEvent.EventType.INSERT, attrs);

        preInsertUpdate(evt, attrs);

        // Store modification text as an event's property
        org.netbeans.lib.editor.util.swing.DocumentUtilities.addEventPropertyStorage(evt);
        org.netbeans.lib.editor.util.swing.DocumentUtilities.putEventProperty(evt, String.class, text);
        if (postModificationDepth > 0) {
            DocumentPostModificationUtils.markPostModification(evt);
        }

        if (edit != null) {
            evt.addEdit(edit);

            lastModifyUndoEdit = edit; // #8692 check last modify undo edit
        }

        modified = true;

        if (atomicDepth > 0) {
            ensureAtomicEditsInited();
            atomicEdits.addEdit(evt); // will be added
        }

        insertUpdate(evt, attrs);

        evt.end();

        fireInsertUpdate(evt);

        boolean isComposedText = ((attrs != null)
                && (attrs.isDefined(StyleConstants.ComposedTextAttribute)));

        if (composedText && !isComposedText) {
            composedText = false;
        }
        if (!composedText && isComposedText) {
            composedText = true;
        }

        if (atomicDepth == 0 && !isComposedText) { // !!! check
            fireUndoableEditUpdate(new UndoableEditEvent(this, evt));
        }

        postModificationDepth++;
        try {
            if (postModificationDocumentListener != null) {
                postModificationDocumentListener.insertUpdate(evt);
            }
            if (postModificationDocumentListenerList.getListenerCount() > 0) {
                for (DocumentListener listener : postModificationDocumentListenerList.getListeners()) {
                    listener.insertUpdate(evt);
                }
            }
        } finally {
            postModificationDepth--;
        }
    }
    
    private void appendContext(StringBuilder sb, int offset) {
        CharSequence docText = org.netbeans.lib.editor.util.swing.DocumentUtilities.getText(this);
        int contextLen = 20; // Context in forward and backward directions
        int back = contextLen;
        int endOffset = offset;
        int startOffset = offset;
        while (back > 0 && startOffset > 0) {
            if (docText.charAt(startOffset) == '\n') {
                break;
            }
            startOffset--;
            back--;
        }
        int docTextLen = docText.length();
        int forward = contextLen;
        while (forward > 0 && endOffset < docTextLen) {
            if (docText.charAt(endOffset++) == '\n') {
                break;
            }
            forward--;
        }
        if (startOffset > 0) {
            sb.append("...");
        }
        CharSequenceUtilities.debugText(sb, docText.subSequence(startOffset, offset));
        sb.append("|"); // Denote caret
        CharSequenceUtilities.debugText(sb, docText.subSequence(offset, endOffset));
        if (endOffset < docTextLen) {
            sb.append("...");
        }
    }

    public void checkTrailingSpaces(int offset) {
        try {
            int lineNum = Utilities.getLineOffset(this, offset);
            int lastEditedLine = lastPositionEditedByTyping != null ? Utilities.getLineOffset(this, lastPositionEditedByTyping.getOffset()) : -1;
            if (lastEditedLine != -1 && lastEditedLine != lineNum) {
                // clear trailing spaces in the last edited line
                Element root = getDefaultRootElement();
                Element elem = root.getElement(lastEditedLine);
                int start = elem.getStartOffset();
                int end = elem.getEndOffset();
                String line = getText(start, end - start);

                int endIndex = line.length() - 1;
                if (endIndex >= 0 && line.charAt(endIndex) == '\n') {
                    endIndex--;
                    if (endIndex >= 0 && line.charAt(endIndex) == '\r') {
                        endIndex--;
                    }
                }

                int startIndex = endIndex;
                while (startIndex >= 0 && Character.isWhitespace(line.charAt(startIndex)) && line.charAt(startIndex) != '\n' &&
                        line.charAt(startIndex) != '\r') {
                    startIndex--;
                }
                startIndex++;
                if (startIndex >= 0 && startIndex <= endIndex) {
                    remove(start + startIndex, endIndex - startIndex + 1);
                }
            }
        } catch (BadLocationException e) {
            LOG.log(Level.WARNING, null, e);
        }
    }

    /** Removes portion of a document */
    public @Override void remove(int offset, int length) throws BadLocationException {
//        if (LOG_EDT.isLoggable(Level.FINE)) { // Only permit operations in EDT
//            if (!SwingUtilities.isEventDispatchThread()) {
//                throw new IllegalStateException("BaseDocument.insertString not in EDT: offset=" + // NOI18N
//                        offset + ", len=" + length); // NOI18N
//            }
//        }

        // Always acquire atomic lock (it simplifies processing and improves readability)
        atomicLockImpl();
        try {
            checkModifiable(offset);
            DocumentFilter filter = getDocumentFilter();
            if (filter != null) {
                filter.remove(getFilterBypass(), offset, length);
            } else {
                handleRemove(offset, length);
            }
        } finally {
            atomicUnlockImpl(true);
        }
    }
    
    void handleRemove(int offset, int length) throws BadLocationException {
        if (length == 0) {
            return;
        }
        if (length < 0) {
            throw new IllegalArgumentException("len=" + length + " < 0"); // NOI18N
        }
        if (offset < 0) {
            throw new BadLocationException("Wrong remove position " + offset + " < 0", offset); // NOI18N
        }
        if (offset + length > getLength()) {
            throw new BadLocationException("Wrong (offset+length)=" + (offset+length) +
                    " > getLength()=" + getLength(), offset + length); // NOI18N
        }

        incrementDocVersion();

        int docLen = getLength();
        if (offset < 0 || offset > docLen) {
            throw new BadLocationException("Wrong remove position " + offset, offset); // NOI18N
        }
        if (offset + length > docLen) {
            throw new BadLocationException("End offset of removed text " // NOI18N
                    + (offset + length) + " > getLength()=" + docLen, // NOI18N
                    offset + length); // NOI18N
        }

        preRemoveCheck(offset, length);

        BaseDocumentEvent evt = getDocumentEvent(offset, length, DocumentEvent.EventType.REMOVE, null);
        // Store modification text as an event's property
        org.netbeans.lib.editor.util.swing.DocumentUtilities.addEventPropertyStorage(evt);
        String removedText = getText(offset, length);
        org.netbeans.lib.editor.util.swing.DocumentUtilities.putEventProperty(evt, String.class, removedText);
        if (postModificationDepth > 0) {
            DocumentPostModificationUtils.markPostModification(evt);
        }

        removeUpdate(evt);

        UndoableEdit edit = ((EditorDocumentContent) getContent()).remove(offset, removedText);
        if (edit != null) {
            evt.addEdit(edit);

            lastModifyUndoEdit = edit; // #8692 check last modify undo edit
        }

        if (LOG.isLoggable(Level.FINE)) {
            StringBuilder sb = new StringBuilder(200);
            sb.append("remove(): doc="); // NOI18N
            appendInfoTerse(sb);
            sb.append(",origDocLen=").append(docLen); // NOI18N
            sb.append(", offset=").append(Utilities.offsetToLineColumnString(this, offset)); // NOI18N
            sb.append(",len=").append(length); // NOI18N
            if (!debugNoText) {
                sb.append(" \"");
                appendContext(sb, offset);
                sb.append("\" - \""); // NOI18N
                CharSequenceUtilities.debugText(sb, ((ContentEdit) edit).getText());
                sb.append("\""); // NOI18N
            }

            if (debugStack) {
                LOG.log(Level.FINE, sb.toString(), new Throwable("Remove text")); // NOI18N
            } else {
                LOG.log(Level.FINE, sb.toString());
            }
        }

        if (atomicDepth > 0) { // add edits as soon as possible
            ensureAtomicEditsInited();
            atomicEdits.addEdit(evt); // will be added
        }

        postRemoveUpdate(evt);

        evt.end();

        fireRemoveUpdate(evt);

        postModificationDepth++;
        try {
            if (postModificationDocumentListener != null) {
                postModificationDocumentListener.removeUpdate(evt);
            }
            if (postModificationDocumentListenerList.getListenerCount() > 0) {
                for (DocumentListener listener : postModificationDocumentListenerList.getListeners()) {
                    listener.removeUpdate(evt);
                }
            }
        } finally {
            postModificationDepth--;
        }

        if (atomicDepth == 0 && !composedText) {
            fireUndoableEditUpdate(new UndoableEditEvent(this, evt));
        }
    }

    public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
        // Always acquire atomic lock (it simplifies processing and improves readability)
        atomicLockImpl();
        try {
            checkModifiable(offset);
            DocumentFilter filter = getDocumentFilter();
            if (filter != null) {
                filter.replace(getFilterBypass(), offset, length, text, attrs);
            } else {
                handleRemove(offset, length);
                handleInsertString(offset, text, attrs);
            }
        } finally {
            atomicUnlockImpl(true);
        }
    }
    
    private void checkModifiable(int offset) throws BadLocationException {
        if (!modifiable) {
            throw new GuardedException("Modification prohibited", offset); // NOI18N
        }
    }

    private DocumentFilter.FilterBypass getFilterBypass() {
        if (filterBypass == null) {
            filterBypass = new FilterBypassImpl();
        }
        return filterBypass;
    }

    /** This method is called automatically before the document
    * insertion occurs and can be used to revoke the insertion before it occurs
    * by throwing the BadLocationException.
    * @param offset position where the insertion will be done
    * @param text string to be inserted
    * @param a attributes of the inserted text
    */
    protected void preInsertCheck(int offset, String text, AttributeSet a)
    throws BadLocationException {
    }

    /** This method is called automatically before the document
    * removal occurs and can be used to revoke the removal before it occurs
    * by throwing the BadLocationException.
    * @param offset position where the insertion will be done
    * @param len length of the removal
    */
    protected void preRemoveCheck(int offset, int len)
    throws BadLocationException {
    }

    protected @Override void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
        super.insertUpdate(chng, attr);

        BaseDocumentEvent baseE = (BaseDocumentEvent)chng;

        lineRootElement.insertUpdate(baseE, baseE, attr);

        fixLineSyntaxState.update(false);
        chng.addEdit(fixLineSyntaxState.createAfterLineUndo());
        fixLineSyntaxState = null;

        firePreInsertUpdate(chng);
    }

    protected void preInsertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
        fixLineSyntaxState = new FixLineSyntaxState(chng);
        chng.addEdit(fixLineSyntaxState.createBeforeLineUndo());
    }
    
    void firePreRemoveUpdate(DefaultDocumentEvent chng) {
        // Notify the remove update listeners - before the actual remove happens
        // so that it adheres to removeUpdate() logic; also the listeners can check
        // positions' offsets before the actual removal happens.
        for (DocumentListener listener: updateDocumentListenerList.getListeners()) {
            listener.removeUpdate(chng);
        }
    }

    void firePreInsertUpdate(DefaultDocumentEvent chng) {
        for (DocumentListener listener: updateDocumentListenerList.getListeners()) {
            listener.insertUpdate(chng);
        }
    }

    protected @Override void removeUpdate(DefaultDocumentEvent chng) {
        super.removeUpdate(chng);
        firePreRemoveUpdate(chng);
        // Remember the line changes here but add them to chng during postRemoveUpdate()
        // in order to satisfy the legacy syntax update mechanism
        removeUpdateLineUndo = lineRootElement.legacyRemoveUpdate(chng);

        fixLineSyntaxState = new FixLineSyntaxState(chng);
        chng.addEdit(fixLineSyntaxState.createBeforeLineUndo());
    }

    protected @Override void postRemoveUpdate(DefaultDocumentEvent chng) {
        super.postRemoveUpdate(chng);

        // addEdit() for previously remembered removeUpdateLineUndo
        if (removeUpdateLineUndo != null) {
            chng.addEdit(removeUpdateLineUndo);
            removeUpdateLineUndo = null;
        }

        fixLineSyntaxState.update(false);
        chng.addEdit(fixLineSyntaxState.createAfterLineUndo());
        fixLineSyntaxState = null;
    }

    public String getText(int[] block) throws BadLocationException {
        return getText(block[0], block[1] - block[0]);
    }

    /**
     * @param pos position of the first character to get.
     * @param len number of characters to obtain.
     * @return array with the requested characters.
     */
    public char[] getChars(int pos, int len) throws BadLocationException {
        char[] chars = new char[len];
        getChars(pos, chars, 0, len);
        return chars;
    }

    /**
     * @param block two-element array with starting and ending offset
     * @return array with the requested characters.
     */
    public char[] getChars(int[] block) throws BadLocationException {
        return getChars(block[0], block[1] - block[0]);
    }

    /**
     * @param pos position of the first character to get.
     * @param ret destination array
     * @param offset offset in the destination array.
     * @param len number of characters to obtain.
     * @return array with the requested characters.
     */
    public void getChars(int pos, char ret[], int offset, int len)
    throws BadLocationException {
        DocumentUtilities.copyText(this, pos, pos + len, ret, offset);
    }

    /** Find something in document using a finder.
    * @param finder finder to be used for the search
    * @param startPos position in the document where the search will start
    * @param limitPos position where the search will be end with reporting
    *   that nothing was found.
    */
    public int find(Finder finder, int startPos, int limitPos)
    throws BadLocationException {
        int docLen = getLength();
        if (limitPos == -1) {
            limitPos = docLen;
        }
        if (startPos == -1) {
            startPos = docLen;
        }

        if (finder instanceof AdjustFinder) {
            if (startPos == limitPos) { // stop immediately
                finder.reset(); // reset() should be called in all the cases
                return -1; // must stop here because wouldn't know if fwd/bwd search?
            }

            boolean forwardAdjustedSearch = (startPos < limitPos);
            startPos = ((AdjustFinder)finder).adjustStartPos(this, startPos);
            limitPos = ((AdjustFinder)finder).adjustLimitPos(this, limitPos);
            boolean voidSearch = (forwardAdjustedSearch ? (startPos >= limitPos) : (startPos <= limitPos));
            if (voidSearch) {
                finder.reset();
                return -1;
            }
        }

        finder.reset();
        if (startPos == limitPos) {
            return -1;
        }

        Segment text = new Segment();
        int gapStart = ((EditorDocumentContent)getContent()).getCharContentGapStart();
        if (gapStart == -1) {
            throw new IllegalStateException("Cannot get gapStart"); // NOI18N
        }

        int pos = startPos; // pos at which the search starts (continues)
        boolean fwdSearch = (startPos <= limitPos); // forward search
        if (fwdSearch) {
            while (pos >= startPos && pos < limitPos) {
                int p0; // low bound
                int p1; // upper bound
                if (pos < gapStart) { // part below gap
                    p0 = startPos;
                    p1 = Math.min(gapStart, limitPos);
                } else { // part above gap
                    p0 = Math.max(gapStart, startPos);
                    p1 = limitPos;
                }

                getText(p0, p1 - p0, text);
                pos = finder.find(p0 - text.offset, text.array,
                        text.offset, text.offset + text.count, pos, limitPos);

                if (finder.isFound()) {
                    return pos;
                }
            }

        } else { // backward search limitPos < startPos
            pos--; // start one char below the upper bound
            while (limitPos <= pos && pos <= startPos) {
                int p0; // low bound
                int p1; // upper bound
                if (pos < gapStart) { // part below gap
                    p0 = limitPos;
                    p1 = Math.min(gapStart, startPos);
                } else { // part above gap
                    p0 = Math.max(gapStart, limitPos);
                    p1 = startPos;
                }

                getText(p0, p1 - p0, text);
                pos = finder.find(p0 - text.offset, text.array,
                        text.offset, text.offset + text.count, pos, limitPos);

                if (finder.isFound()) {
                    return pos;
                }
            }
        }

        return -1; // position outside bounds => not found
    }

    /** Fire the change event to repaint the given block of text.
     * @deprecated Please use JTextComponent.getUI().damageRange() instead.
     */
    public void repaintBlock(int startOffset, int endOffset) {
        BaseDocumentEvent evt = getDocumentEvent(startOffset,
                endOffset - startOffset, DocumentEvent.EventType.CHANGE, null);
        fireChangedUpdate(evt);
    }

    public void print(PrintContainer container) {
        print(container, true, true,0,getLength());
    }

    /**
     * Print into given container.
     *
     * @param container printing container into which the printing will be done.
     * @param usePrintColoringMap use printing coloring settings instead
     *  of the regular ones.
     * @param lineNumberEnabled if set to false the line numbers will not be printed.
     *  If set to true the visibility of line numbers depends on the settings
     *  for the line number visibility.
     * @param startOffset start offset of text to print
     * @param endOffset end offset of text to print
     */
    public void print(PrintContainer container, boolean usePrintColoringMap, boolean lineNumberEnabled, int startOffset,
                      int endOffset) {
        readLock();
        try {
            EditorUI editorUI;
            EditorKit kit = getEditorKit();
            if (kit instanceof BaseKit) {
                editorUI = ((BaseKit) kit).createPrintEditorUI(this, usePrintColoringMap, lineNumberEnabled);
            } else {
                editorUI = new EditorUI(this, usePrintColoringMap, lineNumberEnabled);
            }

            DrawGraphics.PrintDG printDG = new DrawGraphics.PrintDG(container);
            DrawEngine.getDrawEngine().draw(printDG, editorUI, startOffset, endOffset, 0, 0, Integer.MAX_VALUE);
        } catch (BadLocationException e) {
            LOG.log(Level.WARNING, null, e);
        } finally {
            readUnlock();
        }
    }

    /**
     * Print into given container.
     *
     * @param container printing container into which the printing will be done.
     * @param usePrintColoringMap use printing coloring settings instead
     *  of the regular ones.
     * @param lineNumberEnabled if null, the visibility of line numbers is the same as it is given by settings
     *  for the line number visibility, otherwise the visibility equals the boolean value of the parameter
     * @param startOffset start offset of text to print
     * @param endOffset end offset of text to print
     */
    public void print(PrintContainer container, boolean usePrintColoringMap, Boolean lineNumberEnabled, int startOffset,
                      int endOffset) {
        readLock();
        try {
            boolean lineNumberEnabledPar = true;
            boolean forceLineNumbers = false;
            if (lineNumberEnabled != null) {
                lineNumberEnabledPar = lineNumberEnabled.booleanValue();
                forceLineNumbers = lineNumberEnabled.booleanValue();
            }

            EditorUI editorUI;
            EditorKit kit = getEditorKit();
            if (kit instanceof BaseKit) {
                editorUI = ((BaseKit) kit).createPrintEditorUI(this, usePrintColoringMap, lineNumberEnabledPar);
            } else {
                editorUI = new EditorUI(this, usePrintColoringMap, lineNumberEnabledPar);
            }

            if (forceLineNumbers) {
                editorUI.setLineNumberVisibleSetting(true);
                editorUI.setLineNumberEnabled(true);
                editorUI.updateLineNumberWidth(0);
            }

            DrawGraphics.PrintDG printDG = new DrawGraphics.PrintDG(container);
            DrawEngine.getDrawEngine().draw(printDG, editorUI, startOffset, endOffset, 0, 0, Integer.MAX_VALUE);
        } catch (BadLocationException e) {
            LOG.log(Level.WARNING, null, e);
        } finally {
            readUnlock();
        }
    }

    /** Create biased position in document */
    public Position createPosition(int offset, Position.Bias bias) throws BadLocationException {
        EditorDocumentContent content = (EditorDocumentContent) getContent();
        Position pos;
        if (bias == Position.Bias.Forward) {
            pos = content.createPosition(offset);
        } else {
            pos = content.createBackwardBiasPosition(offset);
        }
        return pos;
    }

    /** Return array of root elements - usually only one */
    public @Override Element[] getRootElements() {
        Element[] elems = new Element[1];
        elems[0] = getDefaultRootElement();
        return elems;
    }

    /** Return default root element */
    public @Override Element getDefaultRootElement() {
        if (defaultRootElem == null) {
            defaultRootElem = lineRootElement;
        }
        return defaultRootElem;
    }

    /** Runs the runnable under read lock. */
    public @Override void render(Runnable r) {
        readLock();
        try {
            r.run();
        } finally {
            readUnlock();
        }
    }

    /** Runs the runnable under write lock. This is a stronger version
    * of the runAtomicAsUser() method, because if there any locked sections
    * in the documents this methods breaks the modification locks and modifies
    * the document.
    * If there are any excpeptions thrown during the processing of the runnable,
    * all the document modifications are rolled back automatically.
    */
    public void runAtomic(Runnable r) {
        runAtomicAsUser(r);
    }

    /** Runs the runnable under write lock.
    * If there are any excpeptions thrown during the processing of the runnable,
    * all the document modifications are rolled back automatically.
    */
    public void runAtomicAsUser(Runnable r) {
        atomicLockImpl ();
        try {
            r.run();
        // Only attempt to recover (undo the document modifications) from runtime exceptions.
        // Do not attempt to recover from java.lang.Error or other Throwable subclasses.
        } catch (RuntimeException ex) {
            boolean completed = false;
            try {
                breakAtomicLock();
                completed = true;
            } finally {
                if (completed) {
                    throw ex;
                } else {
                    // Log thrown exception in case breakAtomicLock() throws an exception by itself.
                    LOG.log(Level.INFO, "Runtime exception thrown in BaseDocument.runAtomicAsUser() leading to breakAtomicLock():", ex);
                }
            }
        } finally {
            atomicUnlockImpl ();
        }
    }

    /** Insert contents of reader at specified position into document.
    * @param reader reader from which data will be read
    * @param pos on which position that data will be inserted
    */
    public void read(Reader reader, int pos)
    throws IOException, BadLocationException {
        extWriteLock();
        try {

            if (pos < 0 || pos > getLength()) {
                throw new BadLocationException("BaseDocument.read()", pos); // NOI18N
            }
            ReadWriteBuffer buffer = ReadWriteUtils.read(reader);
            if (!inited) { // Fill line-separator properties
                String lineSeparator = ReadWriteUtils.findFirstLineSeparator(buffer);
                if (lineSeparator == null) {
                    lineSeparator = (String) getProperty(FileObject.DEFAULT_LINE_SEPARATOR_ATTR);
                    if (lineSeparator == null) {
                        lineSeparator = ReadWriteUtils.getSystemLineSeparator();
                    }
                }
                putProperty(BaseDocument.READ_LINE_SEPARATOR_PROP, lineSeparator);
            }
            if (debugRead) {
                String ls = (String) getProperty(READ_LINE_SEPARATOR_PROP);
                if (ls != null) {
                    ls = org.netbeans.lib.editor.util.CharSequenceUtilities.debugText(text);
                }
                LOG.log(Level.INFO, "BaseDocument.read(): Will insert {0} chars, lineSeparatorProperty: \"{1}\", StreamDescriptionProperty: {2}\n",
                        new Object[] { buffer.length(), ls, getProperty(StreamDescriptionProperty)} );
            }
            insertString(pos, buffer.toString(), null);
            inited = true; // initialized but not modified

            // Workaround for #138951:
            // BaseDocument.read method is only called when loading the document from a file
            // or from JEditorPane.setText(), which two operations are equivalent in terms
            // that they reset document's content from an external source. Therefore the list
            // of remebered modified regions should be cleared.

            // Reset modified regions accounting after the initial load
            Boolean inPaste = BaseKit.IN_PASTE.get();
            if (inPaste == null || !inPaste) {
                ModRootElement modElementRoot = ModRootElement.get(this);
                if (modElementRoot != null) {
                    modElementRoot.resetMods(null);
                }
            }
            lastModifyUndoEdit = null;
        } finally {
            extWriteUnlock();
        }
    }

    /** Write part of the document into specified writer.
    * @param writer writer into which data will be written.
    * @param pos from which position get the data
    * @param len how many characters write
    */
    public void write(Writer writer, int pos, int len)
    throws IOException, BadLocationException {
        readLock();
        try {

            if ((pos < 0) || ((pos + len) > getLength())) {
                throw new BadLocationException("BaseDocument.write()", pos); // NOI18N
            }
            String lineSeparator = (String) getProperty(BaseDocument.WRITE_LINE_SEPARATOR_PROP);
            if (lineSeparator == null) {
                lineSeparator = (String) getProperty(BaseDocument.READ_LINE_SEPARATOR_PROP);
                if (lineSeparator == null) {
                    lineSeparator = (String) getProperty(FileObject.DEFAULT_LINE_SEPARATOR_ATTR);
                    if (lineSeparator == null) {
                        lineSeparator = ReadWriteUtils.getSystemLineSeparator();
                    }
                }
            }
            CharSequence docText = (CharSequence) getProperty(CharSequence.class);
            // Skip extra '\n' (added by AbstractDocument convention) at the end of char sequence
            ReadWriteBuffer buffer = ReadWriteUtils.convertFromNewlines(docText, pos, pos + len, lineSeparator);
            ReadWriteUtils.write(writer, buffer);
            writer.flush();
        } finally {
            readUnlock();
        }
    }

    /** Invalidate the state-infos in all the syntax-marks
     * in the whole document. The Syntax can call this method
     * if it changes its internal state in the way that affects
     * the future returned tokens. The syntax-state-info in all
     * the marks is reset and it will be lazily restored when necessary.
     */
    public void invalidateSyntaxMarks() {
        extWriteLock();
        try {
            FixLineSyntaxState.invalidateAllSyntaxStateInfos(this);
            BaseDocumentEvent evt = getDocumentEvent(0, getLength(), DocumentEvent.EventType.CHANGE, null);
            fireChangedUpdate(evt);
        } finally {
          extWriteUnlock();
        }
    }

    /** Get the number of spaces the TAB character ('\t') visually represents.
     * This is related to SettingsNames.TAB_SIZE setting.
     */
    public int getTabSize() {
        return tabSize;
    }

    /** Get the width of one indentation level.
     * The algorithm first checks whether there's a value for the INDENT_SHIFT_WIDTH
     * setting. If so it uses it, otherwise it uses formatter.getSpacesPerTab().
     *
     * @see getTabSize()
     * @deprecated Please use Editor Indentation API instead, for details see
     *   Editor Indentation.
     */
    public int getShiftWidth() {
        return shiftWidth;
    }

    /**
     * @deprecated Don't use implementation class of editor kits. Use mime type,
     *   MimePath and MimeLookup.
     */
    public final Class getKitClass() {
        return getEditorKit().getClass();
    }

    private EditorKit getEditorKit() {
        EditorKit editorKit = MimeLookup.getLookup(mimeType).lookup(EditorKit.class);
        if (editorKit == null) {
            // Try 'text/plain'
            LOG.log(Level.CONFIG, "No registered editor kit for ''{0}'', trying ''text/plain''.", mimeType);
            editorKit = MimeLookup.getLookup("text/plain").lookup(EditorKit.class); //NOI18N
            if (editorKit == null) {
                LOG.config("No registered editor kit for 'text/plain', using default.");
                editorKit = new PlainEditorKit();
            }
        }
        return editorKit;
    }

//    private static String s2s(Object o) {
//        return o == null ? "null" : o.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(o));
//    }
//
    private void setMimeType(String mimeType) {
        if (!this.mimeType.equals(mimeType)) {
//            String oldMimeType = this.mimeType;
            this.mimeType = mimeType;

//            new Throwable("~~~ setMimeType: '" + oldMimeType + "' -> '" + mimeType + "'").printStackTrace(System.out);
//
            if (prefs != null && weakPrefsListener != null) {
                try {
                    prefs.removePreferenceChangeListener(weakPrefsListener);
//                    System.out.println("~~~ setMimeType: " + s2s(prefs) + " removing " + s2s(weakPrefsListener));
                } catch (IllegalArgumentException e) {
//                    System.out.println("~~~ IAE: doc=" + s2s(this) + ", '" + oldMimeType + "' -> '" + this.mimeType + "', prefs=" + s2s(prefs) + ", wpl=" + s2s(weakPrefsListener));
                }
                weakPrefsListener = null;
            }
            prefs = MimeLookup.getLookup(this.mimeType).lookup(Preferences.class);
            prefsListener.preferenceChange(null);
//            System.out.println("~~~ setMimeType: '" + this.mimeType + "' -> " + s2s(prefs));

            // reinitialize the document
            EditorKit kit = getEditorKit();
            if (kit instanceof BaseKit) {
                // careful here!! some kits set document's mime type from initDocument
                // even worse, the new mime type can be different from the one passed to the constructor,
                // which will result in recursive call to this method
                ((BaseKit) kit).initDocument(this);
            }

            if (weakPrefsListener == null) {
                weakPrefsListener = WeakListeners.create(PreferenceChangeListener.class, prefsListener, prefs);
                prefs.addPreferenceChangeListener(weakPrefsListener);
//                System.out.println("~~~ setMimeType: " + s2s(prefs) + " adding " + s2s(weakPrefsListener));
            }
        }
    }

    /** This method prohibits merging of the next document modification
    * with the previous one even if it would be normally possible.
    */
    public void resetUndoMerge() {
        undoMergeReset = true;
    }
    
    /* Defined because of the hack for undo()
     * in the BaseDocumentEvent.
     */
    protected @Override void fireChangedUpdate(DocumentEvent e) {
        super.fireChangedUpdate(e);
    }
    protected @Override void fireInsertUpdate(DocumentEvent e) {
        super.fireInsertUpdate(e);
    }
    protected @Override void fireRemoveUpdate(DocumentEvent e) {
        super.fireRemoveUpdate(e);
    }

    protected @Override void fireUndoableEditUpdate(UndoableEditEvent e) {
        // Possibly wrap contained edit
        if (undoEditWrappers != null) {
            UndoableEdit edit = e.getEdit();
            ListUndoableEdit listEdit = null;
            for (UndoableEditWrapper wrapper : undoEditWrappers) {
                UndoableEdit wrapEdit = wrapper.wrap(edit, this);
                if (wrapEdit != edit) {
                    if (listEdit == null) {
                        listEdit = new ListUndoableEdit(edit, wrapEdit);
                    } else {
                        listEdit.setDelegate(wrapEdit);
                    }
                    edit = wrapEdit;
                }
            }
            if (listEdit != null) {
                e = new UndoableEditEvent(this, listEdit);
            }
        }
        
	// Fire to the list of listeners that was used before the atomic lock started
        // This fixes issue #47881 and appears to be somewhat more logical
        // than the default approach to fire all the current listeners
	Object[] listeners = (atomicLockListenerList != null)
            ? atomicLockListenerList
            : listenerList.getListenerList();

	for (int i = listeners.length - 2; i >= 0; i -= 2) {
	    if (listeners[i] == UndoableEditListener.class) {
		((UndoableEditListener)listeners[i + 1]).undoableEditHappened(e);
	    }
	}

        // Since UndoManager may only do um.addEdit() the original resetting
        // in AtomicCompoundEdit.replaceEdit() would not be called in such case.
        undoMergeReset = false;
    }

    /** Extended write locking of the document allowing
    * reentrant write lock acquiring.
    */
    public final void extWriteLock() {
        super.writeLock(); // AD.writeLock() already reentrant for several JDK releases
    }

    /** Extended write unlocking.
    * @see extWriteLock()
    */
    public final void extWriteUnlock() {
        super.writeUnlock(); // AD.writeUnlock() already reentrant for several JDK releases
    }

    /** 
     * 
     * @deprecated Please use {@link BaseDocument#runAtomic(java.lang.Runnable)} instead.
     */
    @Override
    public final void atomicLock () {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.log(Level.FINER, "Use runAtomic() instead of atomicLock()", new Exception()); // NOI18N
        }
        atomicLockImpl(); // Need to be outside synchronized(this) due to VetoableChangeListener firing
    }
    
    final synchronized void atomicLockImpl () {
        if (runExclusiveDepth > 0) {
            throw new IllegalStateException(
                    "Document modifications or atomic locking not allowed in runExclusive()"); // NOI18N
        }
        boolean alreadyAtomicLocker = Thread.currentThread() == getCurrentWriter() && atomicDepth > 0;
        if (alreadyAtomicLocker) {
            atomicDepth++;
        }
        if (!alreadyAtomicLocker) {
            // Starting with openide.text v 6.58 it is no longer necessary to fire vetoable change listener
            // since the BaseDocument extends AbstractDocument and so the openide.text installs
            // a document filter which possibly prevents modifications on readonly files.
//            // Fire VetoableChangeListener outside Document lock
//            VetoableChangeListener l = (VetoableChangeListener) getProperty(MODIFICATION_LISTENER_PROP);
//            boolean modifiableLocal = true;
//            if (l != null) {
//                try {
//                    // Notify modification by Boolean.TRUE
//                    l.vetoableChange(new PropertyChangeEvent(this, "modified", null, Boolean.TRUE));
//                } catch (java.beans.PropertyVetoException ex) {
//                    modifiableLocal = false;
//                }
//            }
            // Acquire writeLock() and increment atomicDepth
            synchronized (this) {
                extWriteLock();
                atomicDepth++;
                if (atomicDepth == 1) { // lock really started
//                    Object o = getProperty(EDITABLE_PROP);
//                    if (o == null) {
//                        o = Boolean.TRUE;
//                    }
//                    modifiableChanged = modifiable != modifiableLocal || 
//                            o != modifiableLocal;                
//                    modifiable = modifiableLocal;
                    fireAtomicLock(atomicLockEventInstance);
                    // Copy the listener list - will be used for firing undo
                    atomicLockListenerList = listenerList.getListenerList();
                }
            }
        }
//        if (modifiableChanged) {
//            putProperty(EDITABLE_PROP, modifiable);
//        }
    }

    /** 
     * 
     * @deprecated Please use {@link BaseDocument#runAtomic(java.lang.Runnable)} instead.
     */
    @Override
    public final synchronized void atomicUnlock () {
        atomicUnlockImpl ();
    }
    
    final void atomicUnlockImpl () {
        atomicUnlockImpl(true);
    }

    final void atomicUnlockImpl (boolean notifyUnmodifyIfNoMods) {
        boolean noModsAndOuterUnlock = false;
        synchronized (this) {
            if (atomicDepth <= 0) {
                throw new IllegalStateException("atomicUnlock() without atomicLock()"); // NOI18N
            }

            if (--atomicDepth == 0) { // lock really ended
                fireAtomicUnlock(atomicLockEventInstance);

                noModsAndOuterUnlock = !checkAndFireAtomicEdits();
                atomicLockListenerList = null;
                extWriteUnlock();
            }
        }

        if (notifyUnmodifyIfNoMods && noModsAndOuterUnlock) {
            // Notify unmodification if there were no document modifications
            // inside the atomic section.
            // Fire VetoableChangeListener outside Document lock
            VetoableChangeListener l = (VetoableChangeListener) getProperty(MODIFICATION_LISTENER_PROP);
            if (l != null) {
                try {
                    // Notify unmodification by Boolean.FALSE
                    l.vetoableChange(new PropertyChangeEvent(this, "modified", null, Boolean.FALSE));
                } catch (java.beans.PropertyVetoException ex) {
                    // Ignored (should not be thrown)
                }
            }
        }
    }

    /** Is the document currently atomically locked?
    * It's not synced as this method must be called only from writer thread.
    */
    public final boolean isAtomicLock() {
        return (atomicDepth > 0);
    }

    /** Break the atomic lock so that doc is no longer in atomic mode.
    * All the performed changes are rolled back automatically.
    * Even after calling this method, the atomicUnlock() must still be called.
    * This method is not synced as it must be called only from writer thread.
    */
    public final void breakAtomicLock() {
        undoAtomicEdits();
    }

    public @Override void atomicUndo() {
        breakAtomicLock();
    }

    /**
     * Adapts old AtomicLockListener to the new API; wraps the old listener into a new interface
     */
    private static class OldListenerAdapter implements org.netbeans.api.editor.document.AtomicLockListener {
        private final AtomicLockListener delegate;

        public OldListenerAdapter(AtomicLockListener delegate) {
            this.delegate = delegate;
        }

        public void atomicLock(org.netbeans.api.editor.document.AtomicLockEvent evt) {
            delegate.atomicLock((AtomicLockEvent)evt);
        }

        public void atomicUnlock(org.netbeans.api.editor.document.AtomicLockEvent evt) {
            delegate.atomicUnlock((AtomicLockEvent)evt);
        }

        @Override
        public int hashCode() {
            return delegate.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final OldListenerAdapter other = (OldListenerAdapter) obj;
            if (!Objects.equals(this.delegate, other.delegate)) {
                return false;
            }
            return true;
        }
    }

    public void addAtomicLockListener(org.netbeans.api.editor.document.AtomicLockListener l) {
        listenerList.add(org.netbeans.api.editor.document.AtomicLockListener.class, l);
    }

    public void removeAtomicLockListener(org.netbeans.api.editor.document.AtomicLockListener l) {
        listenerList.remove(org.netbeans.api.editor.document.AtomicLockListener.class, l);
    }

    /**
     * @param l 
     * @deprecated use LineDocumentUtils.as(doc, AtomicLockDocument.class).addAtomicLockListener(l);
     */
    @Deprecated
    public @Override void addAtomicLockListener(AtomicLockListener l) {
        addAtomicLockListener(new OldListenerAdapter(l));
    }

    /**
     * @param l 
     * @deprecated use LineDocumentUtils.as(doc, AtomicLockDocument.class).removeAtomicLockListener(l);
     */
    @Deprecated
    public @Override void removeAtomicLockListener(AtomicLockListener l) {
        removeAtomicLockListener(new OldListenerAdapter(l));
    }

    private void fireAtomicLock(AtomicLockEvent evt) {
        EventListener[] listeners = listenerList.getListeners(org.netbeans.api.editor.document.AtomicLockListener.class);
        int cnt = listeners.length;
        for (int i = 0; i < cnt; i++) {
            ((org.netbeans.api.editor.document.AtomicLockListener)listeners[i]).atomicLock(evt);
        }
    }

    private void fireAtomicUnlock(AtomicLockEvent evt) {
        EventListener[] listeners = listenerList.getListeners(org.netbeans.api.editor.document.AtomicLockListener.class);
        int cnt = listeners.length;
        for (int i = 0; i < cnt; i++) {
            ((org.netbeans.api.editor.document.AtomicLockListener)listeners[i]).atomicUnlock(evt);
        }
    }

    protected final int getAtomicDepth() {
        return atomicDepth;
    }

    void runExclusive(Runnable r) {
        boolean writeLockDone = false;
        synchronized (this) {
            Thread currentWriter = getCurrentWriter();
            if (currentWriter != Thread.currentThread()) {
                assert (runExclusiveDepth == 0) : "runExclusiveDepth=" + runExclusiveDepth + " != 0"; // NOI18N
                writeLock();
                writeLockDone = true;
            }
            runExclusiveDepth++;   
        }
        try {
            r.run();
        } finally {
            runExclusiveDepth--;
            if (writeLockDone) {
                writeUnlock();
                assert (runExclusiveDepth == 0) : "runExclusiveDepth=" + runExclusiveDepth + " != 0"; // NOI18N
            }
            
        }
    }

    @Override
    public void addDocumentListener(DocumentListener listener) {
        if (LOG_LISTENER.isLoggable(Level.FINE)) {
            LOG_LISTENER.fine("ADD DocumentListener of class " + listener.getClass() + " to existing " +
                    org.netbeans.lib.editor.util.swing.DocumentUtilities.getDocumentListenerCount(this) +
                    " listeners. Listener: " + listener + '\n'
            );
            if (LOG_LISTENER.isLoggable(Level.FINER)) {
                LOG_LISTENER.log(Level.FINER, "    StackTrace:\n", new Exception());
            }
        }
	if (!org.netbeans.lib.editor.util.swing.DocumentUtilities.addPriorityDocumentListener(
                this, listener, DocumentListenerPriority.DEFAULT))
            super.addDocumentListener(listener);
    }

    @Override
    public void removeDocumentListener(DocumentListener listener) {
        if (LOG_LISTENER.isLoggable(Level.FINE)) {
            LOG_LISTENER.fine("REMOVE DocumentListener of class " + listener.getClass() + " from existing " +
                    org.netbeans.lib.editor.util.swing.DocumentUtilities.getDocumentListenerCount(this) +
                    " listeners. Listener: " + listener + '\n'
            );
            if (LOG_LISTENER.isLoggable(Level.FINER)) {
                LOG_LISTENER.log(Level.FINER, "    StackTrace:\n", new Exception());
            }
        }
	if (!org.netbeans.lib.editor.util.swing.DocumentUtilities.removePriorityDocumentListener(
                this, listener, DocumentListenerPriority.DEFAULT))
            super.removeDocumentListener(listener);
    }

    protected BaseDocumentEvent createDocumentEvent(int pos, int length,
            DocumentEvent.EventType type) {
        return new BaseDocumentEvent(this, pos, length, type);
    }

    /* package */ final BaseDocumentEvent getDocumentEvent(int pos, int length, DocumentEvent.EventType type, AttributeSet attribs) {
        BaseDocumentEvent bde = createDocumentEvent(pos, length, type);
        bde.attachChangeAttribs(attribs);
        return bde;
    }

    /**
     * Set or clear a special document listener that gets notified
     * after the modification and that is allowed to do further
     * mutations to the document.
     * 
* Additional mutations will be made in a single atomic transaction * with an original mutation. *
* This functionality may be used for example by code templates * to synchronize other regions of the document with the one * currently being modified. *
* If there is an active post modification document listener * then each document modification is encapsulated in an atomic lock * transaction automatically to allow further changes inside a transaction. * * @deprecated Use addPostModificationDocumentListener(DocumentListener) */ public void setPostModificationDocumentListener(DocumentListener listener) { this.postModificationDocumentListener = listener; } /** * Add a special document listener that gets notified * after the modification and that is allowed to do further * mutations to the document. *
* Additional mutations will be made in a single atomic transaction * with an original mutation. *
* This functionality may be used for example by code templates * to synchronize other regions of the document with the one * currently being modified. *
* If there is an active post modification document listener * then each document modification is encapsulated in an atomic lock * transaction automatically to allow further changes inside a transaction. * * @since 1.25 */ public void addPostModificationDocumentListener(DocumentListener listener) { postModificationDocumentListenerList.add(listener); } public void removePostModificationDocumentListener(DocumentListener listener) { postModificationDocumentListenerList.remove(listener); } /** * Add a special document listener that gets notified after physical * insertion/removal has been done but when the document event * (which is a {@link javax.swing.undo.CompoundEdit}) is still * opened for extra undoable edits that can be added by the clients (listeners). * * @param listener non-null listener to be added. * @since 1.27 */ public void addUpdateDocumentListener(DocumentListener listener) { updateDocumentListenerList.add(listener); } public void removeUpdateDocumentListener(DocumentListener listener) { updateDocumentListenerList.remove(listener); } @Override public void addUndoableEditListener(UndoableEditListener listener) { super.addUndoableEditListener(listener); if (LOG.isLoggable(Level.FINE)) { UndoableEditListener[] listeners = getUndoableEditListeners(); if (listeners.length > 1) { // Having two UE listeners may be dangerous - for example // having two undo managers attached at once will lead to strange errors // since only one of the UMs will work normally while processing // in the other one will be (typically silently) failing. LOG.log(Level.INFO, "Two or more UndoableEditListeners attached", new Exception()); // NOI18N } } } /** * Add a custom undoable edit during atomic lock of the document. *
* For example code templates use this method to mark an insertion of a code template * skeleton into the document. Once the edit gets undone the CT editing will be cancelled. * * @param edit non-null undoable edit. * @throws IllegalStateException if the document is not under atomic lock. * @since 1.29 */ public void addUndoableEdit(UndoableEdit edit) { if (!isAtomicLock()) throw new IllegalStateException("This method can only be called under atomic-lock."); // NOI18N ensureAtomicEditsInited(); atomicEdits.addEdit(edit); } /** Was the document modified by either insert/remove * but not the initial read)? */ public boolean isModified() { return modified; } public @Override Element getParagraphElement(int pos) { return lineRootElement.getElement(lineRootElement.getElementIndex(pos)); } /** Returns object which represent list of annotations which are * attached to this document. * @return object which represent attached annotations */ public Annotations getAnnotations() { synchronized (annotationsLock) { if (annotations == null) { annotations = new Annotations(this); } return annotations; } } /** * @see LineRootElement#prepareSyntax() */ void prepareSyntax(Segment text, Syntax syntax, int reqPos, int reqLen, boolean forceLastBuffer, boolean forceNotLastBuffer) throws BadLocationException { FixLineSyntaxState.prepareSyntax(this, text, syntax, reqPos, reqLen, forceLastBuffer, forceNotLastBuffer); } int getTokenSafeOffset(int offset) { return FixLineSyntaxState.getTokenSafeOffset(this, offset); } /** Get position on line from visual column. This method can be used * only for superfixed font i.e. all characters of all font styles * have the same width. * @param visCol visual column * @param startLineOffset position of line start * @return position on line for particular x-coord */ int getOffsetFromVisCol(int visCol, int startLineOffset) throws BadLocationException { int tabSize = getTabSize(); CharSequence docText = org.netbeans.lib.editor.util.swing.DocumentUtilities.getText(this); int docTextLen = docText.length(); int curVisCol = 0; int offset = startLineOffset; for (; offset < docTextLen; offset++) { if (curVisCol >= visCol) { return offset; } char ch = docText.charAt(offset); switch (ch) { case '\t': curVisCol = (curVisCol + tabSize) / tabSize * tabSize; break; case '\n': return offset; default: // #17356 int codePoint; if (Character.isHighSurrogate(ch) && offset + 1 < docTextLen) { codePoint = Character.toCodePoint(ch, docText.charAt(++offset)); } else { codePoint = ch; } int w = WcwdithUtil.wcwidth(codePoint); curVisCol += w > 0 ? w : 0; break; } } return offset; } /** Get visual column from position. This method can be used * only for superfixed font i.e. all characters of all font styles * have the same width. * @param offset position for which the visual column should be returned * the function itself computes the begining of the line first */ int getVisColFromPos(int offset) throws BadLocationException { if (offset < 0 || offset > getLength()) { throw new BadLocationException("Invalid offset", offset); // NOI18N } int startLineOffset = Utilities.getRowStart(this, offset); int tabSize = getTabSize(); CharSequence docText = org.netbeans.lib.editor.util.swing.DocumentUtilities.getText(this); int visCol = 0; for (int i = startLineOffset; i < offset; i++) { char ch = docText.charAt(i); if (ch == '\t') { visCol = (visCol + tabSize) / tabSize * tabSize; } else { // #17356 int codePoint; if (Character.isHighSurrogate(ch) && i + 1 < docText.length()) { codePoint = Character.toCodePoint(ch, docText.charAt(++i)); } else { codePoint = ch; } int w = WcwdithUtil.wcwidth(codePoint); visCol += w > 0 ? w : 0; } } return visCol; } protected Dictionary createDocumentProperties(Dictionary origDocumentProperties) { return new LazyPropertyMap(origDocumentProperties); } private void ensureAtomicEditsInited() { if (atomicEdits == null) { atomicEdits = new AtomicCompoundEdit(); } } private boolean checkAndFireAtomicEdits() { if (atomicEdits != null && atomicEdits.size() > 0) { // Some edits performed atomicEdits.end(); AtomicCompoundEdit nonEmptyAtomicEdits = atomicEdits; atomicEdits = null; // Clear the var to allow doc.runAtomic() in undoableEditHappened() fireUndoableEditUpdate(new UndoableEditEvent(this, nonEmptyAtomicEdits)); return true; } else { return false; } } private void undoAtomicEdits() { if (atomicEdits != null && atomicEdits.size() > 0) { atomicEdits.end(); if (atomicEdits.canUndo()) { atomicEdits.undo(); } else { LOG.log(Level.WARNING, "Cannot UNDO: " + atomicEdits.toString() + // NOI18N " Edits: " + atomicEdits.getEdits(), // NOI18N new CannotUndoException()); } atomicEdits = null; } } void clearAtomicEdits() { atomicEdits = null; } UndoableEdit startOnSaveTasks() { assert (atomicDepth > 0); // Should only be called under atomic lock // If there would be any pending edits // fire them so that they don't clash with on-save tasks in undo manager. // Pending edits could occur due to an outer atomic lock // around CES.saveDocument(). Anyway it would generally be an undesirable situation // due to possibly invalid lock order (doc's atomic lock would then precede on-save tasks' locks). checkAndFireAtomicEdits(); ensureAtomicEditsInited(); return atomicEdits; } void endOnSaveTasks(boolean success) { if (success) { // Possibly fire atomic edit checkAndFireAtomicEdits(); } else { // Undo edits contained in atomic edit undoAtomicEdits(); } } UndoableEdit markAtomicEditsNonSignificant() { assert (atomicDepth > 0); // Should only be called under atomic lock ensureAtomicEditsInited(); atomicEdits.setSignificant(false); return atomicEdits; } void appendInfoTerse(StringBuilder sb) { sb.append(getClass().getSimpleName()).append("@").append(System.identityHashCode(this)); sb.append(",version=").append(org.netbeans.lib.editor.util.swing.DocumentUtilities.getDocumentVersion(this)); sb.append(",StreamDesc=").append(getProperty(StreamDescriptionProperty)); } public @Override String toString() { return super.toString() + ", mimeType='" + mimeType + "'" + //NOI18N ", kitClass=" + deprecatedKitClass + // NOI18N ", length=" + getLength() + // NOI18N ", version=" + org.netbeans.lib.editor.util.swing.DocumentUtilities.getDocumentVersion(this) + // NOI18N ", file=" + getProperty(StreamDescriptionProperty); //NOI18N } /** Detailed debug info about the document */ public String toStringDetail() { return toString() + ", content:\n " + getContent().toString(); } /* package */ void incrementDocVersion() { ((AtomicLong) getProperty(VERSION_PROP)).incrementAndGet(); ((AtomicLong) getProperty(LAST_MODIFICATION_TIMESTAMP_PROP)).set(System.currentTimeMillis()); } /** Compound edit that write-locks the document for the whole processing * of its undo operation. */ class AtomicCompoundEdit extends StableCompoundEdit { private static final int MERGE_INDEX_NOT_INITIALIZED = -1; /** * Marker value for mergeEditIndex to signal that the merge is not possible * (value must be less than MERGE_INDEX_NOT_INITIALIZED). */ private static final int MERGE_PROHIBITED = -2; private UndoableEdit previousEdit; private boolean nonSignificant; /** * If an edit gets added that prohibits merge then this flag is set to true. */ private int mergeEditIndex = MERGE_INDEX_NOT_INITIALIZED; public @Override void undo() throws CannotUndoException { atomicLockImpl (); try { TokenHierarchyControl thcInactive = thcInactive(); try { super.undo(); } finally { if (thcInactive != null) { thcInactive.setActive(true); } } } finally { atomicUnlockImpl (); } if (previousEdit != null) { previousEdit.undo(); } } public @Override void redo() throws CannotRedoException { if (previousEdit != null) { previousEdit.redo(); } atomicLockImpl (); try { TokenHierarchyControl thcInactive = thcInactive(); try { super.redo(); } finally { if (thcInactive != null) { thcInactive.setActive(true); } } } finally { atomicUnlockImpl (); } } private TokenHierarchyControl thcInactive() { TokenHierarchyControl thc = null; if (getEdits().size() > DEACTIVATE_LEXER_THRESHOLD) { MutableTextInput input = (MutableTextInput) BaseDocument.this.getProperty(MutableTextInput.class); if (input != null && (thc = input.tokenHierarchyControl()) != null && thc.isActive()) { thc.setActive(false); } else { thc = null; } } return thc; } public @Override void die() { super.die(); if (previousEdit != null) { // Should not always be previousEdit != this ?? // Apparently not, see the stacktrace in #145634. if (previousEdit != this) { previousEdit.die(); } previousEdit = null; } } public int size() { return getEdits().size(); } @Override public boolean addEdit(UndoableEdit anEdit) { if (super.addEdit(anEdit)) { if (mergeEditIndex >= MERGE_INDEX_NOT_INITIALIZED) { // Valid or not inited if (anEdit instanceof BaseDocumentEvent) { mergeEditIndex = size() - 1; } else if (anEdit.getClass() != BaseDocument.AtomicCompoundEdit.class && !CaretUndo.isCaretUndoEdit(anEdit) ) { mergeEditIndex = MERGE_PROHIBITED; // Do not allow merging if there are unknown undoable edits } } return true; } return false; } public @Override boolean replaceEdit(UndoableEdit anEdit) { if (nonSignificant) { // Non-significant edit must be replacing previousEdit = anEdit; // Becomes significant nonSignificant = false; return true; } if (!undoMergeReset && mergeEditIndex >= 0) { // Only merge if this edit contains BaseDocumentEvent child item List thisEdits = getEdits(); BaseDocumentEvent thisMergeEdit = (BaseDocumentEvent) thisEdits.get(mergeEditIndex); if (anEdit instanceof BaseDocument.AtomicCompoundEdit) { BaseDocument.AtomicCompoundEdit anAtomicEdit = (BaseDocument.AtomicCompoundEdit)anEdit; List anAtomicEditChildren = anAtomicEdit.getEdits(); for (int i = 0; i < anAtomicEditChildren.size(); i++) { UndoableEdit child = (UndoableEdit)anAtomicEditChildren.get(i); if (child instanceof BaseDocumentEvent && thisMergeEdit.canMerge((BaseDocumentEvent)child)) { previousEdit = anEdit; return true; } } } else if (anEdit instanceof BaseDocumentEvent) { BaseDocumentEvent evt = (BaseDocumentEvent)anEdit; if (thisMergeEdit.canMerge(evt)) { previousEdit = anEdit; return true; } } } return false; } @Override public boolean isSignificant() { return !nonSignificant; } public void setSignificant(boolean significant) { this.nonSignificant = !significant; } BaseDocumentEvent getMergeEdit() { return (BaseDocumentEvent) ((mergeEditIndex >= 0) ? getEdits().get(mergeEditIndex) : null); } } // End of AtomicCompoundEdit /** Property evaluator is useful for lazy evaluation * of properties of the document when * {@link javax.swing.text.Document#getProperty(java.lang.String)} * is called. */ public static interface PropertyEvaluator { public Object getValue(); } private static final class MimeTypePropertyEvaluator implements BaseDocument_PropertyHandler { private final BaseDocument doc; private String hackMimeType = null; public MimeTypePropertyEvaluator(BaseDocument baseDocument) { this.doc = baseDocument; } public @Override Object getValue() { if (hackMimeType == null) { return doc.mimeType; } else { return hackMimeType; } } public @Override Object setValue(Object value) { String mimeType = value == null ? null : value.toString(); assert MimePath.validate(mimeType) : "Invalid mime type: '" + mimeType + "'"; //NOI18N // XXX: hack to support Tools-Options' "testNNNN_*" mime types boolean hackNewMimeType = mimeType != null && mimeType.startsWith("test"); //NOI18N // // Do not change anything, just sanity checks // if (value != null && LOG.isLoggable(Level.WARNING)) { // String msg = null; // if (doc.editorKit != null) { // msg = "Trying to set " + MIME_TYPE_PROP + " property to '" + mimeType + //NOI18N // "' on properly initialized document " + doc; //NOI18N // } else { // // baseDocument initialized by the deprecated constructor // if (!mimeType.equals(doc.getEditorKit().getContentType())) { // msg = "Trying to set " + MIME_TYPE_PROP + " property to '" + mimeType + //NOI18N // "', which is inconsistent with the content type of document's editor kit, document = " + doc; //NOI18N // } // } // if (msg != null && !hackNewMimeType) { // LOG.log(Level.WARNING, null, new Throwable(msg)); // } // } String oldValue = (String) getValue(); if (hackNewMimeType) { hackMimeType = mimeType; } else { doc.setMimeType(mimeType); } return oldValue; } } // End of MimeTypePropertyEvaluator class protected static class LazyPropertyMap extends Hashtable { private PropertyChangeSupport pcs = null; protected LazyPropertyMap(Dictionary dict) { super(5); Enumeration en = dict.keys(); while (en.hasMoreElements()) { Object key = en.nextElement(); super.put(key, dict.get(key)); } } public @Override Object get(Object key) { Object val = super.get(key); if (val instanceof PropertyEvaluator) { val = ((PropertyEvaluator)val).getValue(); } return val; } public @Override Object put(Object key, Object value) { if (key == PropertyChangeSupport.class && value instanceof PropertyChangeSupport) { pcs = (PropertyChangeSupport) value; } Object old = null; boolean usePlainPut = true; if (key != null) { Object val = super.get(key); if (val instanceof BaseDocument_PropertyHandler) { old = ((BaseDocument_PropertyHandler) val).setValue(value); usePlainPut = false; } } if (usePlainPut) { old = super.put(key, value); } if (key instanceof String) { if (pcs != null) { pcs.firePropertyChange((String) key, old, value); } } return old; } } // End of LazyPropertyMap class private static final class Accessor extends EditorPackageAccessor { @Override public UndoableEdit BaseDocument_markAtomicEditsNonSignificant(BaseDocument doc) { return doc.markAtomicEditsNonSignificant(); } @Override public void BaseDocument_clearAtomicEdits(BaseDocument doc) { doc.clearAtomicEdits(); } @Override public MarkVector BaseDocument_getMarksStorage(BaseDocument doc) { return null; // doc.marksStorage no longer supported; DrawEngine no longer used } @Override public Mark BaseDocument_getMark(BaseDocument doc, MultiMark multiMark) { return null; // doc.marks.get(multiMark) no longer supported; DrawEngine no longer used } @Override public void Mark_insert(Mark mark, BaseDocument doc, int pos) throws InvalidMarkException, BadLocationException { mark.insert(doc, pos); } @Override public void ActionFactory_reformat(Reformat formatter, Document doc, int startPos, int endPos, AtomicBoolean canceled) throws BadLocationException { ActionFactory.reformat(formatter, doc, startPos, endPos, canceled); } @Override public Object BaseDocument_newServices(BaseDocument doc) { return new ServicesImpl(doc); } @Override public int MarkBlockChain_adjustPos(MarkBlockChain chain, int pos, boolean thisBlock) { return thisBlock ? chain.adjustToBlockHead(pos) : chain.adjustToPrevBlockEnd(pos); } } // XXX: the same as the one in CloneableEditorSupport private static final class PlainEditorKit extends DefaultEditorKit implements ViewFactory { static final long serialVersionUID = 1L; PlainEditorKit() { } /** @return cloned instance */ public @Override Object clone() { return new PlainEditorKit(); } /** @return this (I am the ViewFactory) */ public @Override ViewFactory getViewFactory() { return this; } /** Plain view for the element */ public @Override View create(Element elem) { return new WrappedPlainView(elem); } /** Set to a sane font (not proportional!). */ public @Override void install(JEditorPane pane) { super.install(pane); pane.setFont(new Font("Monospaced", Font.PLAIN, pane.getFont().getSize() + 1)); //NOI18N } } // End of PlainEditorKit class class FilterBypassImpl extends DocumentFilter.FilterBypass { public Document getDocument() { return BaseDocument.this; } public void remove(int offset, int length) throws BadLocationException { handleRemove(offset, length); } public void insertString(int offset, String string, AttributeSet attrs) throws BadLocationException { handleInsertString(offset, string, attrs); } public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException { handleRemove(offset, length); handleInsertString(offset, text, attrs); } } /** * Implementation of EditorDocumentServices for BaseDocument. * * @author Miloslav Metelka */ private static final class BaseDocumentServices implements EditorDocumentServices { static final EditorDocumentServices INSTANCE = new BaseDocumentServices(); @Override public void runExclusive(Document doc, Runnable r) { BaseDocument bDoc = (BaseDocument) doc; bDoc.runExclusive(r); } @Override public void resetUndoMerge(Document doc) { BaseDocument bDoc = (BaseDocument) doc; bDoc.resetUndoMerge(); } @Override public UndoableEdit startOnSaveTasks(Document doc) { BaseDocument bDoc = (BaseDocument) doc; return bDoc.startOnSaveTasks(); } @Override public void endOnSaveTasks(Document doc, boolean success) { BaseDocument bDoc = (BaseDocument) doc; bDoc.endOnSaveTasks(success); } } static final class ServicesImpl implements CharClassifier, org.netbeans.api.editor.document.AtomicLockDocument { private final BaseDocument doc; public ServicesImpl(BaseDocument doc) { this.doc = doc; } @Override public boolean isIdentifierPart(char ch) { return doc.isIdentifierPart(ch); } @Override public boolean isWhitespace(char ch) { return doc.isWhitespace(ch); } public void atomicLock() { doc.atomicLockImpl(); } public void atomicUnlock() { doc.atomicUnlockImpl(); } @Override public void atomicUndo() { doc.atomicUndo(); } @Override public void runAtomic(Runnable r) { doc.runAtomic(r); } @Override public void runAtomicAsUser(Runnable r) { doc.runAtomicAsUser(r); } @Override public void addAtomicLockListener(org.netbeans.api.editor.document.AtomicLockListener l) { doc.addAtomicLockListener(l); } @Override public void removeAtomicLockListener(org.netbeans.api.editor.document.AtomicLockListener l) { doc.removeAtomicLockListener(l); } @Override public Document getDocument() { return doc; } /* @Override public int find(org.netbeans.api.editor.document.Finder f, int from, int limit) throws BadLocationException { return doc.find2(f, from, limit); } */ } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy