/*
 * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
 * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.CSSStyleDeclarationTextEditor = class CSSStyleDeclarationTextEditor extends WebInspector.Object
{
    constructor(delegate, style, element)
    {
        super();

        this._element = element || document.createElement("div");
        this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName);
        this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName);
        this._element.addEventListener("mousedown", this._handleMouseDown.bind(this));
        this._element.addEventListener("mouseup", this._handleMouseUp.bind(this));

        this._mouseDownCursorPosition = null;

        this._showsImplicitProperties = true;
        this._alwaysShowPropertyNames = {};
        this._filterResultPropertyNames = null;
        this._sortProperties = false;

        this._prefixWhitespace = "\n";
        this._suffixWhitespace = "\n";
        this._linePrefixWhitespace = "";

        this._delegate = delegate || null;

        this._codeMirror = CodeMirror(this.element, {
            readOnly: true,
            lineWrapping: true,
            mode: "css-rule",
            electricChars: false,
            indentWithTabs: false,
            indentUnit: 4,
            smartIndent: false,
            matchBrackets: true,
            autoCloseBrackets: true
        });

        this._codeMirror.addKeyMap({
            "Enter": this._handleEnterKey.bind(this),
            "Shift-Enter": this._insertNewlineAfterCurrentLine.bind(this),
            "Shift-Tab": this._handleShiftTabKey.bind(this),
            "Tab": this._handleTabKey.bind(this)
        });

        this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this);
        this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this);

        this._completionController.noEndingSemicolon = true;

        this._jumpToSymbolTrackingModeEnabled = false;
        this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName;
        this._tokenTrackingController.mouseOverDelayDuration = 0;
        this._tokenTrackingController.mouseOutReleaseDelayDuration = 0;
        this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;

        // Make sure CompletionController adds event listeners first.
        // Otherwise we end up in race conditions during complete or delete-complete phases.
        this._codeMirror.on("change", this._contentChanged.bind(this));
        this._codeMirror.on("blur", this._editorBlured.bind(this));
        this._codeMirror.on("beforeChange", this._handleBeforeChange.bind(this));

        if (typeof this._delegate.cssStyleDeclarationTextEditorFocused === "function")
            this._codeMirror.on("focus", this._editorFocused.bind(this));

        this.style = style;
    }

    // Public

    get element()
    {
        return this._element;
    }

    get delegate()
    {
        return this._delegate;
    }

    set delegate(delegate)
    {
        this._delegate = delegate || null;
    }

    get style()
    {
        return this._style;
    }

    set style(style)
    {
        if (this._style === style)
            return;

        if (this._style) {
            this._style.removeEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
            if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
                WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
        }

        this._style = style || null;

        if (this._style) {
            this._style.addEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
            if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
                WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
        }

        this._updateJumpToSymbolTrackingMode();

        this._resetContent();
    }

    get focused()
    {
        return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused");
    }

    get alwaysShowPropertyNames()
    {
        return Object.keys(this._alwaysShowPropertyNames);
    }

    set alwaysShowPropertyNames(alwaysShowPropertyNames)
    {
        this._alwaysShowPropertyNames = (alwaysShowPropertyNames || []).keySet();

        this._resetContent();
    }

    get showsImplicitProperties()
    {
        return this._showsImplicitProperties;
    }

    set showsImplicitProperties(showsImplicitProperties)
    {
        if (this._showsImplicitProperties === showsImplicitProperties)
            return;

        this._showsImplicitProperties = showsImplicitProperties;

        this._resetContent();
    }

    get sortProperties()
    {
        return this._sortProperties;
    }

    set sortProperties(sortProperties)
    {
        if (this._sortProperties === sortProperties)
            return;

        this._sortProperties = sortProperties;

        this._resetContent();
    }

    focus()
    {
        this._codeMirror.focus();
    }

    refresh()
    {
        this._resetContent();
    }

    updateLayout(force)
    {
        this._codeMirror.refresh();
    }

    highlightProperty(property)
    {
        function propertiesMatch(cssProperty)
        {
            if (cssProperty.enabled && !cssProperty.overridden) {
                if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
                    return true;
            }

            return false;
        }

        function hasMatchingLonghandProperty(cssProperty)
        {
            var cssProperties = cssProperty.relatedLonghandProperties;

            if (!cssProperties.length)
                return false;

            for (var property of cssProperties) {
                if (propertiesMatch(property))
                    return true;
            }

            return false;
        }

        for (var cssProperty of this.style.properties) {
            if (propertiesMatch(cssProperty)) {
                var selection = cssProperty.__propertyTextMarker.find();
                this._codeMirror.setSelection(selection.from, selection.to);
                this.focus();

                return true;
            }
        }

        return false;
    }

    clearSelection()
    {
        this._codeMirror.setCursor({line: 0, ch: 0});
    }

    findMatchingProperties(needle)
    {
        if (!needle) {
            this.resetFilteredProperties();
            return false;
        }

        var propertiesList = this._style.visibleProperties.length ? this._style.visibleProperties : this._style.properties;
        var matchingProperties = [];

        for (var property of propertiesList)
            matchingProperties.push(property.text.includes(needle));

        if (!matchingProperties.includes(true)) {
            this.resetFilteredProperties();
            return false;
        }

        for (var i = 0; i < matchingProperties.length; ++i) {
            var property = propertiesList[i];

            if (matchingProperties[i])
                property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName;
            else
                property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName;

            this._updateTextMarkerForPropertyIfNeeded(property);
        }

        return true;
    }

    resetFilteredProperties()
    {
        var propertiesList = this._style.visibleProperties.length ? this._style.visibleProperties : this._style.properties;

        for (var property of propertiesList) {
            if (property.__filterResultClassName) {
                property.__filterResultClassName = null;
                this._updateTextMarkerForPropertyIfNeeded(property)
            }
        }
    }

    removeNonMatchingProperties(needle)
    {
        this._filterResultPropertyNames = null;

        if (!needle) {
            this._resetContent();
            return false;
        }

        var matchingPropertyNames = [];

        for (var property of this._style.properties) {
            var indexesOfNeedle = property.text.getMatchingIndexes(needle);

            if (indexesOfNeedle.length) {
                matchingPropertyNames.push(property.name);
                property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName;
                property.__filterResultNeedlePosition = {start: indexesOfNeedle, length: needle.length};
            }
        }

        this._filterResultPropertyNames = matchingPropertyNames.length ? matchingPropertyNames.keySet() : {};

        this._resetContent();

        return matchingPropertyNames.length > 0;
    }

    uncommentAllProperties()
    {
        function uncommentProperties(properties)
        {
            if (!properties.length)
                return false;

            for (var property of properties) {
                if (property._commentRange) {
                    this._uncommentRange(property._commentRange);
                    property._commentRange = null;
                }
            }

            return true;
        }

        return uncommentProperties.call(this, this._style.pendingProperties) || uncommentProperties.call(this, this._style.properties);
    }

    commentAllProperties()
    {
        if (!this._style.properties.length)
            return false;

        for (var property of this._style.properties) {
            if (property.__propertyTextMarker)
                this._commentProperty(property);
        }

        return true;
    }

    selectFirstProperty()
    {
        var line = this._codeMirror.getLine(0);
        var trimmedLine = line.trimRight();

        if (!line || !trimmedLine.trimLeft().length)
            this.clearSelection();

        var index = line.indexOf(":");
        var cursor = {line: 0, ch: 0};

        this._codeMirror.setSelection(cursor, {line: 0, ch: index < 0 || this._textAtCursorIsComment(this._codeMirror, cursor) ? trimmedLine.length : index});
    }

    selectLastProperty()
    {
        var line = this._codeMirror.lineCount() - 1;
        var lineText = this._codeMirror.getLine(line);
        var trimmedLine = lineText.trimRight();

        var lastAnchor;
        var lastHead;

        if (this._textAtCursorIsComment(this._codeMirror, {line, ch: line.length})) {
            lastAnchor = 0;
            lastHead = line.length;
        } else {
            var colon = /(?::\s*)/.exec(lineText);
            lastAnchor = colon ? colon.index + colon[0].length : 0;
            lastHead = trimmedLine.length - trimmedLine.endsWith(";");
        }

        this._codeMirror.setSelection({line, ch: lastAnchor}, {line, ch: lastHead});
    }

    // Protected

    didDismissPopover(popover)
    {
        if (popover === this._colorPickerPopover)
            this._colorPickerPopover = null;
        if (popover === this._cubicBezierEditorPopover)
            this._cubicBezierEditorPopover = null;
    }

    completionControllerCompletionsHidden(completionController)
    {
        var styleText = this._style.text;
        var currentText = this._formattedContent();

        // If the style text and the current editor text differ then we need to commit.
        // Otherwise we can just update the properties that got skipped because a completion
        // was pending the last time _propertiesChanged was called.
        if (styleText !== currentText)
            this._commitChanges();
        else
            this._propertiesChanged();
    }

    // Private

    _textAtCursorIsComment(codeMirror, cursor)
    {
        var token = codeMirror.getTokenTypeAt(cursor);
        return token && token.includes("comment");
    }

    _highlightNextNameOrValue(codeMirror, cursor, text)
    {
        var nextAnchor;
        var nextHead;

        if (this._textAtCursorIsComment(codeMirror, cursor)) {
            nextAnchor = 0;
            nextHead = text.length;
        } else {
            var colonIndex = text.indexOf(":");
            var substringIndex = colonIndex >= 0 && cursor.ch >= colonIndex ? colonIndex : 0;

            var regExp = /(?:[^:;\s]\s*)+/g;
            regExp.lastIndex = substringIndex;
            var match = regExp.exec(text);

            nextAnchor = match.index;
            nextHead = nextAnchor + match[0].length;
        }

        codeMirror.setSelection({line: cursor.line, ch: nextAnchor}, {line: cursor.line, ch: nextHead});
    }

    _handleMouseDown(event)
    {
        if (this._codeMirror.options.readOnly)
            return;

        let cursor = this._codeMirror.coordsChar({left: event.x, top: event.y});
        let line = this._codeMirror.getLine(cursor.line);
        let trimmedLine = line.trimRight();
        if (!trimmedLine.trimLeft().length || cursor.ch !== trimmedLine.length)
            return;

        this._mouseDownCursorPosition = cursor;
    }

    _handleMouseUp(event)
    {
        if (this._codeMirror.options.readOnly || !this._mouseDownCursorPosition)
            return;

        let cursor = this._codeMirror.coordsChar({left: event.x, top: event.y});
        if (this._mouseDownCursorPosition.line === cursor.line && this._mouseDownCursorPosition.ch === cursor.ch) {
            let nextLine = this._codeMirror.getLine(cursor.line + 1);
            if (cursor.line < this._codeMirror.lineCount() - 1 && (!nextLine || !nextLine.trim().length)) {
                this._codeMirror.setCursor({line: cursor.line + 1, ch: 0});
            } else {
                let line = this._codeMirror.getLine(cursor.line);
                let replacement = "\n";
                if (!line.trimRight().endsWith(";") && !this._textAtCursorIsComment(this._codeMirror, cursor))
                    replacement = ";" + replacement;

                this._codeMirror.replaceRange(replacement, cursor);
            }
        }

        this._mouseDownCursorPosition = null;
    }

    _handleBeforeChange(codeMirror, change)
    {
        if (change.origin !== "+delete" || this._completionController.isShowingCompletions())
            return CodeMirror.Pass;

        if (!change.to.line && !change.to.ch) {
            if (codeMirror.lineCount() === 1)
                return CodeMirror.Pass;

            var line = codeMirror.getLine(change.to.line);
            if (line && line.trim().length)
                return CodeMirror.Pass;

            codeMirror.execCommand("deleteLine");
            return;
        }

        var marks = codeMirror.findMarksAt(change.to);
        if (!marks.length)
            return CodeMirror.Pass;

        for (var mark of marks)
            mark.clear();
    }

    _handleEnterKey(codeMirror)
    {
        var cursor = codeMirror.getCursor();
        var line = codeMirror.getLine(cursor.line);
        var trimmedLine = line.trimRight();
        var hasEndingSemicolon = trimmedLine.endsWith(";");

        if (!trimmedLine.trimLeft().length)
            return CodeMirror.Pass;

        if (hasEndingSemicolon && cursor.ch === trimmedLine.length - 1)
            ++cursor.ch;

        if (cursor.ch === trimmedLine.length) {
            var replacement = "\n";

            if (!hasEndingSemicolon && !this._textAtCursorIsComment(this._codeMirror, cursor))
                replacement = ";" + replacement;

            this._codeMirror.replaceRange(replacement, cursor);
            return;
        }

        return CodeMirror.Pass;
    }

    _insertNewlineAfterCurrentLine(codeMirror)
    {
        var cursor = codeMirror.getCursor();
        var line = codeMirror.getLine(cursor.line);
        var trimmedLine = line.trimRight();

        cursor.ch = trimmedLine.length;

        if (cursor.ch) {
            var replacement = "\n";

            if (!trimmedLine.endsWith(";") && !this._textAtCursorIsComment(this._codeMirror, cursor))
                replacement = ";" + replacement;

            this._codeMirror.replaceRange(replacement, cursor);
            return;
        }

        return CodeMirror.Pass;
    }

    _handleShiftTabKey(codeMirror)
    {
        function switchRule()
        {
            if (this._delegate && typeof this._delegate.cssStyleDeclarationTextEditorSwitchRule === "function") {
                this._delegate.cssStyleDeclarationTextEditorSwitchRule(true);
                return;
            }

            return CodeMirror.Pass;
        }

        var cursor = codeMirror.getCursor();
        var line = codeMirror.getLine(cursor.line);
        var previousLine = codeMirror.getLine(cursor.line - 1);

        if (!line && !previousLine && !cursor.line)
            return switchRule.call(this);

        var trimmedPreviousLine = previousLine ? previousLine.trimRight() : "";
        var previousAnchor;
        var previousHead;
        var isComment = this._textAtCursorIsComment(codeMirror, cursor);

        if (cursor.ch === line.indexOf(":") || line.indexOf(":") < 0 || isComment) {
            if (previousLine) {
                --cursor.line;

                if (this._textAtCursorIsComment(codeMirror, cursor)) {
                    previousAnchor = 0;
                    previousHead = trimmedPreviousLine.length;
                } else {
                    var colon = /(?::\s*)/.exec(previousLine);
                    previousAnchor = colon ? colon.index + colon[0].length : 0;
                    previousHead = trimmedPreviousLine.length - trimmedPreviousLine.endsWith(";");
                }
                
                codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
                return;
            }

            if (cursor.line) {
                codeMirror.setCursor(cursor.line - 1, 0);
                return;
            }

            return switchRule.call(this);
        }

        if (isComment) {
            previousAnchor = 0;
            previousHead = line.length;
        } else {
            var match = /(?:[^:;\s]\s*)+/.exec(line);
            previousAnchor = match.index;
            previousHead = previousAnchor + match[0].length;
        }

        codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
    }

    _handleTabKey(codeMirror)
    {
        function switchRule() {
            if (this._delegate && typeof this._delegate.cssStyleDeclarationTextEditorSwitchRule === "function") {
                this._delegate.cssStyleDeclarationTextEditorSwitchRule();
                return;
            }

            return CodeMirror.Pass;
        }

        var cursor = codeMirror.getCursor();
        var line = codeMirror.getLine(cursor.line);
        var trimmedLine = line.trimRight();
        var lastLine = cursor.line === codeMirror.lineCount() - 1;
        var nextLine = codeMirror.getLine(cursor.line + 1);
        var trimmedNextLine = nextLine ? nextLine.trimRight() : "";

        if (!trimmedLine.trimLeft().length) {
            if (lastLine)
                return switchRule.call(this);

            if (!trimmedNextLine.trimLeft().length) {
                codeMirror.setCursor(cursor.line + 1, 0);
                return;
            }

            ++cursor.line;
            this._highlightNextNameOrValue(codeMirror, cursor, nextLine);
            return;
        }

        if (trimmedLine.endsWith(":")) {
            codeMirror.setCursor(cursor.line, line.length);
            this._completionController._completeAtCurrentPosition(true);
            return;
        }

        var hasEndingSemicolon = trimmedLine.endsWith(";");

        if (cursor.ch >= line.trimRight().length - hasEndingSemicolon) {
            this._completionController.completeAtCurrentPositionIfNeeded().then(function(result) {
                if (result !== WebInspector.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound)
                    return;

                var replacement = "";

                if (!hasEndingSemicolon && !this._textAtCursorIsComment(codeMirror, cursor))
                    replacement += ";";

                if (lastLine)
                    replacement += "\n";

                if (replacement.length)
                    codeMirror.replaceRange(replacement, {line: cursor.line, ch: trimmedLine.length});

                if (!nextLine) {
                    codeMirror.setCursor(cursor.line + 1, 0);
                    return;
                }

                this._highlightNextNameOrValue(codeMirror, {line: cursor.line + 1, ch: 0}, nextLine);
            }.bind(this));

            return;
        }

        this._highlightNextNameOrValue(codeMirror, cursor, line);
    }

    _clearRemoveEditingLineClassesTimeout()
    {
        if (!this._removeEditingLineClassesTimeout)
            return;

        clearTimeout(this._removeEditingLineClassesTimeout);
        delete this._removeEditingLineClassesTimeout;
    }

    _removeEditingLineClasses()
    {
        this._clearRemoveEditingLineClassesTimeout();

        function removeEditingLineClasses()
        {
            var lineCount = this._codeMirror.lineCount();
            for (var i = 0; i < lineCount; ++i)
                this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
        }

        this._codeMirror.operation(removeEditingLineClasses.bind(this));
    }

    _removeEditingLineClassesSoon()
    {
        if (this._removeEditingLineClassesTimeout)
            return;
        this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay);
    }

    _formattedContent()
    {
        // Start with the prefix whitespace we stripped.
        var content = this._prefixWhitespace;

        // Get each line and add the line prefix whitespace and newlines.
        var lineCount = this._codeMirror.lineCount();
        for (var i = 0; i < lineCount; ++i) {
            var lineContent = this._codeMirror.getLine(i);
            content += this._linePrefixWhitespace + lineContent;
            if (i !== lineCount - 1)
                content += "\n";
        }

        // Add the suffix whitespace we stripped.
        content += this._suffixWhitespace;

        return content;
    }

    _commitChanges()
    {
        if (this._commitChangesTimeout) {
            clearTimeout(this._commitChangesTimeout);
            delete this._commitChangesTimeout;
        }

        this._style.text = this._formattedContent();
    }

    _editorBlured(codeMirror)
    {
        // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case.
        if (this._completionController.isHandlingClickEvent())
            return;

        // Reset the content on blur since we stop accepting external changes while the the editor is focused.
        // This causes us to pick up any change that was suppressed while the editor was focused.
        this._resetContent();
        this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.Blurred);
    }

    _editorFocused(codeMirror)
    {
        if (typeof this._delegate.cssStyleDeclarationTextEditorFocused === "function")
            this._delegate.cssStyleDeclarationTextEditorFocused();
    }

    _contentChanged(codeMirror, change)
    {
        // Return early if the style isn't editable. This still can be called when readOnly is set because
        // clicking on a color swatch modifies the text.
        if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent)
            return;

        this._markLinesWithCheckboxPlaceholder();

        this._clearRemoveEditingLineClassesTimeout();
        this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);

        // When the change is a completion change, create color swatches now since the changes
        // will not go through _propertiesChanged until completionControllerCompletionsHidden happens.
        // This way any auto completed colors get swatches right away.
        if (this._completionController.isCompletionChange(change)) {
            this._createColorSwatches(false, change.from.line);
            this._createBezierEditors(false, change.from.line);
        }

        // Use a short delay for user input to coalesce more changes before committing. Other actions like
        // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that
        // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing.
        var delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0;

        // Reset the timeout so rapid changes coalesce after a short delay.
        if (this._commitChangesTimeout)
            clearTimeout(this._commitChangesTimeout);
        this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay);

        this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.ContentChanged);
    }

    _updateTextMarkers(nonatomic)
    {
        function update()
        {
            this._clearTextMarkers(true);

            this._iterateOverProperties(true, function(property) {
                var styleTextRange = property.styleDeclarationTextRange;
                console.assert(styleTextRange);
                if (!styleTextRange)
                    return;

                var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn};
                var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn};

                // Adjust the line position for the missing prefix line.
                if (this._prefixWhitespace) {
                    --from.line;
                    --to.line;
                }

                // Adjust the column for the stripped line prefix whitespace.
                from.ch -= this._linePrefixWhitespace.length;
                to.ch -= this._linePrefixWhitespace.length;

                this._createTextMarkerForPropertyIfNeeded(from, to, property);
            });

            if (!this._codeMirror.getOption("readOnly")) {
                // Look for comments that look like properties and add checkboxes in front of them.
                this._codeMirror.eachLine(function(lineHandler) {
                    this._createCommentedCheckboxMarker(lineHandler);
                }.bind(this));
            }

            // Look for colors and make swatches.
            this._createColorSwatches(true);
            this._createBezierEditors(true);

            this._markLinesWithCheckboxPlaceholder();
        }

        if (nonatomic)
            update.call(this);
        else
            this._codeMirror.operation(update.bind(this));
    }

    _createCommentedCheckboxMarker(lineHandle)
    {
        var lineNumber = lineHandle.lineNo();

        // Since lineNumber can be 0, it is also necessary to check if it is a number before returning.
        if (!lineNumber && isNaN(lineNumber))
            return;

        // Matches a comment like: /* -webkit-foo: bar; */
        var commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g;

        var match = commentedPropertyRegex.exec(lineHandle.text);
        if (!match)
            return;

        while (match) {
            var checkboxElement = document.createElement("input");
            checkboxElement.type = "checkbox";
            checkboxElement.checked = false;
            checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this));

            var from = {line: lineNumber, ch: match.index};
            var to = {line: lineNumber, ch: match.index + match[0].length};

            var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
            checkboxMarker.__propertyCheckbox = true;

            var commentTextMarker = this._codeMirror.markText(from, to);

            checkboxElement.__commentTextMarker = commentTextMarker;

            match = commentedPropertyRegex.exec(lineHandle.text);
        }
    }

    _createColorSwatches(nonatomic, lineNumber)
    {
        function update()
        {
            var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;

            // Look for color strings and add swatches in front of them.
            createCodeMirrorColorTextMarkers(this._codeMirror, range, function(marker, color, colorString) {
                var swatchElement = document.createElement("span");
                swatchElement.title = WebInspector.UIString("Click to select a color. Shift-click to switch color formats.");
                swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName;
                swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this));

                var swatchInnerElement = document.createElement("span");
                swatchInnerElement.style.backgroundColor = colorString;
                swatchElement.appendChild(swatchInnerElement);

                var codeMirrorTextMarker = marker.codeMirrorTextMarker;
                this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement);

                swatchInnerElement.__colorTextMarker = codeMirrorTextMarker;
                swatchInnerElement.__color = color;
            }.bind(this));
        }

        if (nonatomic)
            update.call(this);
        else
            this._codeMirror.operation(update.bind(this));
    }

    _createBezierEditors(nonatomic, lineNumber)
    {
        function update()
        {
            var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;

            // Look for cubic-bezier and timing functions and add cubic-bezier icons in front of them.
            createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range, function(marker, cubicBezier) {
                var bezierMarker = document.createElement("span");
                bezierMarker.title = WebInspector.UIString("Click to open a cubic-bezier editor");
                bezierMarker.className = WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName;
                bezierMarker.addEventListener("click", this._cubicBezierMarkerClicked.bind(this));

                var codeMirrorTextMarker = marker.codeMirrorTextMarker;
                this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, bezierMarker);

                bezierMarker.__textMarker = codeMirrorTextMarker;
                bezierMarker.__bezier = cubicBezier;
            }.bind(this));
        }

        if (nonatomic)
            update.call(this);
        else
            this._codeMirror.operation(update.bind(this));
    }

    _updateTextMarkerForPropertyIfNeeded(property)
    {
        var textMarker = property.__propertyTextMarker;
        console.assert(textMarker);
        if (!textMarker)
            return;

        var range = textMarker.find();
        console.assert(range);
        if (!range)
            return;

        this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property);
    }

    _createTextMarkerForPropertyIfNeeded(from, to, property)
    {
        if (!this._codeMirror.getOption("readOnly")) {
            // Create a new checkbox element and marker.

            console.assert(property.enabled);

            var checkboxElement = document.createElement("input");
            checkboxElement.type = "checkbox";
            checkboxElement.checked = true;
            checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this));
            checkboxElement.__cssProperty = property;

            var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
            checkboxMarker.__propertyCheckbox = true;
        } else if (this._delegate.cssStyleDeclarationTextEditorShouldAddPropertyGoToArrows
                && !property.implicit && typeof this._delegate.cssStyleDeclarationTextEditorShowProperty === "function") {

            var arrowElement = WebInspector.createGoToArrowButton();

            var delegate = this._delegate;
            arrowElement.addEventListener("click", function() {
                delegate.cssStyleDeclarationTextEditorShowProperty(property);
            });

            this._codeMirror.setUniqueBookmark(to, arrowElement);
        }

        function duplicatePropertyExistsBelow(cssProperty)
        {
            var propertyFound = false;

            for (var property of this._style.properties) {
                if (property === cssProperty)
                    propertyFound = true;
                else if (property.name === cssProperty.name && propertyFound)
                    return true;
            }

            return false;
        }

        var propertyNameIsValid = false;
        if (WebInspector.CSSCompletions.cssNameCompletions)
            propertyNameIsValid = WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.name);

        var classNames = ["css-style-declaration-property"];

        if (property.overridden)
            classNames.push("overridden");

        if (property.implicit)
            classNames.push("implicit");

        if (this._style.inherited && !property.inherited)
            classNames.push("not-inherited");

        if (!property.valid && property.hasOtherVendorNameOrKeyword())
            classNames.push("other-vendor");
        else if (!property.valid && (!propertyNameIsValid || duplicatePropertyExistsBelow.call(this, property)))
            classNames.push("invalid");

        if (!property.enabled)
            classNames.push("disabled");

        if (property.__filterResultClassName && !property.__filterResultNeedlePosition)
            classNames.push(property.__filterResultClassName);

        var classNamesString = classNames.join(" ");

        // If there is already a text marker and it's in the same document, then try to avoid recreating it.
        // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause
        // both editors to fight and always recreate their text markers. This isn't really common.
        if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) {
            // If the class name is the same then we don't need to make a new marker.
            if (property.__propertyTextMarker.className === classNamesString)
                return;

            property.__propertyTextMarker.clear();
        }

        var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString});

        propertyTextMarker.__cssProperty = property;
        property.__propertyTextMarker = propertyTextMarker;

        property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this);

        this._removeCheckboxPlaceholder(from.line);

        if (property.__filterResultClassName && property.__filterResultNeedlePosition) {
            for (var needlePosition of property.__filterResultNeedlePosition.start) {
                var start = {line: from.line, ch: needlePosition};
                var end = {line: to.line, ch: start.ch + property.__filterResultNeedlePosition.length};

                this._codeMirror.markText(start, end, {className: property.__filterResultClassName});
            }
        }

        if (property.hasOtherVendorNameOrKeyword() || property.text.trim().endsWith(":"))
            return;

        var propertyHasUnnecessaryPrefix = property.name.startsWith("-webkit-") && WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.canonicalName);

        function generateInvalidMarker(options)
        {
            var invalidMarker = document.createElement("button");
            invalidMarker.className = "invalid-warning-marker";
            invalidMarker.title = options.title;

            if (typeof options.correction === "string") {
                // Allow for blank strings
                invalidMarker.classList.add("clickable");
                invalidMarker.addEventListener("click", function() {
                    this._codeMirror.replaceRange(options.correction, from, to);

                    if (options.autocomplete) {
                        this._codeMirror.setCursor(to);
                        this.focus();
                        this._completionController._completeAtCurrentPosition(true);
                    }
                }.bind(this));
            }

            this._codeMirror.setBookmark(options.position, invalidMarker);
        }

        function instancesOfProperty(propertyName)
        {
            var count = 0;

            for (var property of this._style.properties) {
                if (property.name === propertyName)
                    ++count;
            }

            return count;
        }

        // Number of times this property name is listed in the rule.
        var instances = instancesOfProperty.call(this, property.name);
        var invalidMarkerInfo;

        if (propertyHasUnnecessaryPrefix && !instancesOfProperty.call(this, property.canonicalName)) {
            // This property has a prefix and is valid without the prefix and the rule containing this property does not have the unprefixed version of the property.
            generateInvalidMarker.call(this, {
                position: from,
                title: WebInspector.UIString("The 'webkit' prefix is not necessary.\nClick to insert a duplicate without the prefix."),
                correction: property.text + "\n" + property.text.replace("-webkit-", ""),
                autocomplete: false
            });
        } else if (instances > 1) {
            invalidMarkerInfo = {
                position: from,
                title: WebInspector.UIString("Duplicate property '%s'.\nClick to delete this property.").format(property.name),
                correction: "",
                autocomplete: false
            };
        }

        if (property.valid) {
            if (invalidMarkerInfo)
                generateInvalidMarker.call(this, invalidMarkerInfo);

            return;
        }

        if (propertyNameIsValid) {
            // The property's name is valid but its value is not (either it is not supported for this property or there is no value).
            var semicolon = /:\s*/.exec(property.text);
            var start = {line: from.line, ch: semicolon.index + semicolon[0].length};
            var end = {line: to.line, ch: start.ch + property.value.length};

            this._codeMirror.markText(start, end, {className: "invalid"});

            if (/^(?:\d+)$/.test(property.value)) {
                invalidMarkerInfo = {
                    position: start,
                    title: WebInspector.UIString("The value '%s' needs units.\nClick to add 'px' to the value.").format(property.value),
                    correction: property.name + ": " + property.value + "px;",
                    autocomplete: false
                };
            } else {
                var valueReplacement = property.value.length ? WebInspector.UIString("The value '%s' is not supported for this property.\nClick to delete and open autocomplete.").format(property.value) : WebInspector.UIString("This property needs a value.\nClick to open autocomplete.");

                invalidMarkerInfo = {
                    position: start,
                    title: valueReplacement,
                    correction: property.name + ": ",
                    autocomplete: true
                };
            }
        } else if (!instancesOfProperty.call(this, "-webkit-" + property.name) && WebInspector.CSSCompletions.cssNameCompletions.propertyRequiresWebkitPrefix(property.name)) {
            // The property is valid and exists in the rule while its prefixed version does not.
            invalidMarkerInfo = {
                position: from,
                title: WebInspector.UIString("The 'webkit' prefix is needed for this property.\nClick to insert a duplicate with the prefix."),
                correction: "-webkit-" + property.text + "\n" + property.text,
                autocomplete: false
            };
        } else if (!propertyHasUnnecessaryPrefix && !WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName("-webkit-" + property.name)) {
            // The property either has no prefix and is invalid with a prefix or is invalid without a prefix.
            var closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.name);

            if (closestPropertyName) {
                // The property name has less than 3 other properties that have the same Levenshtein distance.
                invalidMarkerInfo = {
                    position: from,
                    title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format(closestPropertyName),
                    correction: property.text.replace(property.name, closestPropertyName),
                    autocomplete: true
                };
            } else if (property.name.startsWith("-webkit-") && (closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.canonicalName))) {
                // The unprefixed property name has less than 3 other properties that have the same Levenshtein distance.
                invalidMarkerInfo = {
                    position: from,
                    title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format("-webkit-" + closestPropertyName),
                    correction: property.text.replace(property.canonicalName, closestPropertyName),
                    autocomplete: true
                };
            } else {
                // The property name is so vague or nonsensical that there are more than 3 other properties that have the same Levenshtein value.
                invalidMarkerInfo = {
                    position: from,
                    title: WebInspector.UIString("The property '%s' is not supported.").format(property.name),
                    correction: false,
                    autocomplete: false
                };
            }
        }

        if (!invalidMarkerInfo)
            return;

        generateInvalidMarker.call(this, invalidMarkerInfo);
    }

    _clearTextMarkers(nonatomic, all)
    {
        function clear()
        {
            var markers = this._codeMirror.getAllMarks();
            for (var i = 0; i < markers.length; ++i) {
                var textMarker = markers[i];

                if (!all && textMarker.__checkboxPlaceholder) {
                    var position = textMarker.find();

                    // Only keep checkbox placeholders if they are in the first column.
                    if (position && !position.ch)
                        continue;
                }

                if (textMarker.__cssProperty) {
                    textMarker.__cssProperty.removeEventListener(null, null, this);

                    delete textMarker.__cssProperty.__propertyTextMarker;
                    delete textMarker.__cssProperty;
                }

                textMarker.clear();
            }
        }

        if (nonatomic)
            clear.call(this);
        else
            this._codeMirror.operation(clear.bind(this));
    }

    _iterateOverProperties(onlyVisibleProperties, callback)
    {
        var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties;

        if (this._filterResultPropertyNames) {
            properties = properties.filter(function(property) {
                return (!property.implicit || this._showsImplicitProperties) && property.name in this._filterResultPropertyNames;
            }, this);

            if (this._sortProperties)
                properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
        } else if (!onlyVisibleProperties) {
            // Filter based on options only when all properties are used.
            properties = properties.filter(function(property) {
                return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames;
            }, this);

            if (this._sortProperties)
                properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
        }

        for (var i = 0; i < properties.length; ++i) {
            if (callback.call(this, properties[i], i === properties.length - 1))
                break;
        }
    }

    _propertyCheckboxChanged(event)
    {
        var property = event.target.__cssProperty;
        console.assert(property);
        if (!property)
            return;

        this._commentProperty(property);
    }

    _commentProperty(property)
    {
        var textMarker = property.__propertyTextMarker;
        console.assert(textMarker);
        if (!textMarker)
            return;

        // Check if the property has been removed already, like from double-clicking
        // the checkbox and calling this event listener multiple times.
        var range = textMarker.find();
        if (!range)
            return;

        property._commentRange = range;
        property._commentRange.to.ch += 6; // Number of characters added by comments.

        var text = this._codeMirror.getRange(range.from, range.to);

        function update()
        {
            // Replace the text with a commented version.
            this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to);

            // Update the line for any color swatches or cubic-beziers that got removed.
            this._createColorSwatches(true, range.from.line);
            this._createBezierEditors(true, range.from.line);
        }

        this._codeMirror.operation(update.bind(this));
    }

    _propertyCommentCheckboxChanged(event)
    {
        var commentTextMarker = event.target.__commentTextMarker;
        console.assert(commentTextMarker);
        if (!commentTextMarker)
            return;

        // Check if the comment has been removed already, like from double-clicking
        // the checkbox and calling event listener multiple times.
        var range = commentTextMarker.find();
        if (!range)
            return;

        this._uncommentRange(range);
    }

    _uncommentRange(range)
    {
        var text = this._codeMirror.getRange(range.from, range.to);

        // Remove the comment prefix and suffix.
        text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, "");

        // Add a semicolon if there isn't one already.
        if (text.length && text.charAt(text.length - 1) !== ";")
            text += ";";

        function update()
        {
            this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
            this._codeMirror.replaceRange(text, range.from, range.to);

            // Update the line for any color swatches or cubic-beziers that got removed.
            this._createColorSwatches(true, range.from.line);
            this._createBezierEditors(true, range.from.line);
        }

        this._codeMirror.operation(update.bind(this));
    }

    _colorSwatchClicked(event)
    {
        if (this._colorPickerPopover)
            return;

        var swatch = event.target;

        var color = swatch.__color;
        console.assert(color);
        if (!color)
            return;

        var colorTextMarker = swatch.__colorTextMarker;
        console.assert(colorTextMarker);
        if (!colorTextMarker)
            return;

        var range = colorTextMarker.find();
        console.assert(range);
        if (!range)
            return;

        function updateCodeMirror(newColorText)
        {
            function update()
            {
                // The original text marker might have been cleared by a style update,
                // in this case we need to find the new color text marker so we know
                // the right range for the new style color text.
                if (!colorTextMarker || !colorTextMarker.find()) {
                    colorTextMarker = null;

                    var marks = this._codeMirror.findMarksAt(range.from);
                    if (!marks.length)
                        return;

                    for (var i = 0; i < marks.length; ++i) {
                        var mark = marks[i];
                        if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.Color)
                            continue;
                        colorTextMarker = mark;
                        break;
                    }
                }

                if (!colorTextMarker)
                    return;

                // Sometimes we still might find a stale text marker with findMarksAt.
                var newRange = colorTextMarker.find();
                if (!newRange)
                    return;

                range = newRange;

                colorTextMarker.clear();

                this._codeMirror.replaceRange(newColorText, range.from, range.to);

                // The color's text format could have changed, so we need to update the "range"
                // variable to anticipate a different "range.to" property.
                range.to.ch = range.from.ch + newColorText.length;

                colorTextMarker = this._codeMirror.markText(range.from, range.to);

                swatch.__colorTextMarker = colorTextMarker;
            }

            this._codeMirror.operation(update.bind(this));
        }

        if (event.shiftKey || this._codeMirror.getOption("readOnly")) {
            var nextFormat = color.nextFormat();
            console.assert(nextFormat);
            if (!nextFormat)
                return;
            color.format = nextFormat;

            var newColorText = color.toString();

            // Ignore the change so we don't commit the format change. However, any future user
            // edits will commit the color format.
            this._ignoreCodeMirrorContentDidChangeEvent = true;
            updateCodeMirror.call(this, newColorText);
            delete this._ignoreCodeMirrorContentDidChangeEvent;
        } else {
            this._colorPickerPopover = new WebInspector.Popover(this);

            var colorPicker = new WebInspector.ColorPicker;

            colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) {
                updateCodeMirror.call(this, event.data.color.toString());
            }.bind(this));

            var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect());

            this._colorPickerPopover.content = colorPicker.element;
            this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);

            colorPicker.color = color;
        }
    }

    _cubicBezierMarkerClicked(event)
    {
        if (this._cubicBezierEditorPopover)
            return;

        var bezierMarker = event.target;

        var bezier = bezierMarker.__bezier;
        console.assert(bezier);
        if (!bezier)
            return;

        var bezierTextMarker = bezierMarker.__textMarker;
        console.assert(bezierTextMarker);
        if (!bezierTextMarker)
            return;

        var range = bezierTextMarker.find();
        console.assert(range);
        if (!range)
            return;

        function updateCodeMirror(newCubicBezierText)
        {
            function update()
            {
                // The original text marker might have been cleared by a style update,
                // in this case we need to find the new bezier text marker so we know
                // the right range for the new style bezier text.
                if (!bezierTextMarker || !bezierTextMarker.find()) {
                    bezierTextMarker = null;

                    var marks = this._codeMirror.findMarksAt(range.from);
                    if (!marks.length)
                        return;

                    for (var i = 0; i < marks.length; ++i) {
                        var mark = marks[i];
                        if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.CubicBezier)
                            continue;
                        bezierTextMarker = mark;
                        break;
                    }
                }

                if (!bezierTextMarker)
                    return;

                // Sometimes we still might find a stale text marker with findMarksAt.
                var newRange = bezierTextMarker.find();
                if (!newRange)
                    return;

                range = newRange;

                bezierTextMarker.clear();

                this._codeMirror.replaceRange(newCubicBezierText, range.from, range.to);

                // The bezier's text format could have changed, so we need to update the "range"
                // variable to anticipate a different "range.to" property.
                range.to.ch = range.from.ch + newCubicBezierText.length;

                bezierTextMarker = this._codeMirror.markText(range.from, range.to);

                bezierMarker.__textMarker = bezierTextMarker;
            }

            this._codeMirror.operation(update.bind(this));
        }

        this._cubicBezierEditorPopover = new WebInspector.Popover(this);

        var bezierEditor = new WebInspector.BezierEditor;

        bezierEditor.addEventListener(WebInspector.BezierEditor.Event.BezierChanged, function(event) {
            updateCodeMirror.call(this, event.data.bezier.toString());
        }.bind(this));

        var bounds = WebInspector.Rect.rectFromClientRect(bezierMarker.getBoundingClientRect());

        this._cubicBezierEditorPopover.content = bezierEditor.element;
        this._cubicBezierEditorPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);

        bezierEditor.bezier = bezier;
    }

    _propertyOverriddenStatusChanged(event)
    {
        this._updateTextMarkerForPropertyIfNeeded(event.target);
    }

    _propertiesChanged(event)
    {
        // Don't try to update the document while completions are showing. Doing so will clear
        // the completion hint and prevent further interaction with the completion.
        if (this._completionController.isShowingCompletions())
            return;

        // Reset the content if the text is different and we are not focused.
        if (!this.focused && (!this._style.text || this._style.text !== this._formattedContent())) {
            this._resetContent();
            return;
        }

        this._removeEditingLineClassesSoon();

        this._updateTextMarkers();
    }

    _markLinesWithCheckboxPlaceholder()
    {
        if (this._codeMirror.getOption("readOnly"))
            return;

        var linesWithPropertyCheckboxes = {};
        var linesWithCheckboxPlaceholders = {};

        var markers = this._codeMirror.getAllMarks();
        for (var i = 0; i < markers.length; ++i) {
            var textMarker = markers[i];
            if (textMarker.__propertyCheckbox) {
                var position = textMarker.find();
                if (position)
                    linesWithPropertyCheckboxes[position.line] = true;
            } else if (textMarker.__checkboxPlaceholder) {
                var position = textMarker.find();
                if (position)
                    linesWithCheckboxPlaceholders[position.line] = true;
            }
        }

        var lineCount = this._codeMirror.lineCount();

        for (var i = 0; i < lineCount; ++i) {
            if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders)
                continue;

            var position = {line: i, ch: 0};

            var placeholderElement = document.createElement("div");
            placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName;

            var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement);
            placeholderMark.__checkboxPlaceholder = true;
        }
    }

    _removeCheckboxPlaceholder(lineNumber)
    {
        var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0});
        for (var i = 0; i < marks.length; ++i) {
            var mark = marks[i];
            if (!mark.__checkboxPlaceholder)
                continue;

            mark.clear();
            return;
        }
    }

    _formattedContentFromEditor()
    {
        var mapping = {original: [0], formatted: [0]};
        // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
        var indentString = "    ";
        var builder = new WebInspector.FormatterContentBuilder(mapping, [], [], 0, 0, indentString);
        var formatter = new WebInspector.Formatter(this._codeMirror, builder);
        var start = {line: 0, ch: 0};
        var end = {line: this._codeMirror.lineCount() - 1};
        formatter.format(start, end);

        return builder.formattedContent.trim();
    }

    _resetContent()
    {
        if (this._commitChangesTimeout) {
            clearTimeout(this._commitChangesTimeout);
            this._commitChangesTimeout = null;
        }

        this._removeEditingLineClasses();

        // Only allow editing if we have a style, it is editable and we have text range in the stylesheet.
        const readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange;
        this._codeMirror.setOption("readOnly", readOnly);

        if (readOnly) {
            this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
            this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties"));
        } else {
            this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
            this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit"));
        }

        if (!this._style) {
            this._ignoreCodeMirrorContentDidChangeEvent = true;

            this._clearTextMarkers(false, true);
            this._codeMirror.setValue("");
            this._codeMirror.clearHistory();
            this._codeMirror.markClean();

            this._ignoreCodeMirrorContentDidChangeEvent = false;
            return;
        }

        function update()
        {
            // Remember the cursor position/selection.
            let isEditorReadOnly = this._codeMirror.getOption("readOnly");
            let styleText = this._style.text;
            let trimmedStyleText = styleText.trim();

            // We only need to format non-empty styles, but prepare checkbox placeholders
            // in any case because that will indent the cursor when the User starts typing.
            if (!trimmedStyleText && !isEditorReadOnly) {
                this._markLinesWithCheckboxPlaceholder();
                return;
            }

            // Generate formatted content for readonly editors by iterating properties.
            if (isEditorReadOnly) {
                this._codeMirror.setValue("");
                let lineNumber = 0;
                this._iterateOverProperties(false, function(property) {
                    let from = {line: lineNumber, ch: 0};
                    let to = {line: lineNumber};
                    // Readonly properties are pretty printed by `synthesizedText` and not the Formatter.
                    this._codeMirror.replaceRange((lineNumber ? "\n" : "") + property.synthesizedText, from);
                    this._createTextMarkerForPropertyIfNeeded(from, to, property);
                    lineNumber++;
                });
                return;
            }

            let selectionAnchor = this._codeMirror.getCursor("anchor");
            let selectionHead = this._codeMirror.getCursor("head");
            let whitespaceRegex = /\s+/g;

            // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
            this._linePrefixWhitespace = "    ";
            let styleTextPrefixWhitespace = styleText.match(/^\s*/);

            // If there is a match and the style text contains a newline, attempt to pull out the prefix whitespace
            // in front of the first line of CSS to use for every line.  If  there is no newline, we want to avoid
            // adding multiple spaces to a single line CSS rule and instead format it on multiple lines.
            if (styleTextPrefixWhitespace && trimmedStyleText.includes("\n")) {
                let linePrefixWhitespaceMatch = styleTextPrefixWhitespace[0].match(/[^\S\n]+$/);
                if (linePrefixWhitespaceMatch)
                    this._linePrefixWhitespace = linePrefixWhitespaceMatch[0];
            }

            // Set non-optimized, valid and invalid styles in preparation for the Formatter.
            this._codeMirror.setValue(trimmedStyleText);

            // Now the Formatter pretty prints the styles.
            this._codeMirror.setValue(this._formattedContentFromEditor());

            // We need to workaround the fact that...
            // 1) `this._style.properties` only holds valid CSSProperty instances but not
            // comments and invalid properties like `color;`.
            // 2) `_createTextMarkerForPropertyIfNeeded` relies on CSSProperty instances.
            let cssPropertiesMap = new Map();
            this._iterateOverProperties(false, function(cssProperty) {
                cssProperty.__refreshedAfterBlur = false;

                let propertyTextSansWhitespace = cssProperty.text.replace(whitespaceRegex, "");
                let existingProperties = cssPropertiesMap.get(propertyTextSansWhitespace) || [];
                existingProperties.push(cssProperty);

                cssPropertiesMap.set(propertyTextSansWhitespace, existingProperties);
            });

            // Go through the Editor line by line and create TextMarker when a
            // CSSProperty instance for that property exists. If not, then don't create a TextMarker.
            this._codeMirror.eachLine(function(lineHandler) {
                let lineNumber = lineHandler.lineNo();
                let lineContentSansWhitespace = lineHandler.text.replace(whitespaceRegex, "");
                let properties = cssPropertiesMap.get(lineContentSansWhitespace);
                if (!properties) {
                    this._createCommentedCheckboxMarker(lineHandler);
                    return;
                }

                for (let property of properties) {
                    if (property.__refreshedAfterBlur)
                        continue;

                    let from = {line: lineNumber, ch: 0};
                    let to = {line: lineNumber};
                    this._createTextMarkerForPropertyIfNeeded(from, to, property);
                    property.__refreshedAfterBlur = true;
                    break;
                }
            }.bind(this));

            // Look for colors and make swatches.
            this._createColorSwatches(true);
            this._createBezierEditors(true);

            // Restore the cursor position/selection.
            this._codeMirror.setSelection(selectionAnchor, selectionHead);

            // Reset undo history since undo past the reset is wrong when the content was empty before
            // or the content was representing a previous style object.
            this._codeMirror.clearHistory();

            // Mark the editor as clean (unedited state).
            this._codeMirror.markClean();

            this._markLinesWithCheckboxPlaceholder();
        }

        // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
        this._clearTextMarkers(false, true);

        this._ignoreCodeMirrorContentDidChangeEvent = true;
        this._codeMirror.operation(update.bind(this));
        this._ignoreCodeMirrorContentDidChangeEvent = false;
    }

    _updateJumpToSymbolTrackingMode()
    {
        var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled;

        if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation)
            this._jumpToSymbolTrackingModeEnabled = false;
        else
            this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.shiftKey;

        if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) {
            if (this._jumpToSymbolTrackingModeEnabled) {
                this._tokenTrackingController.highlightLastHoveredRange();
                this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly");
            } else {
                this._tokenTrackingController.removeHighlightedRange();
                this._tokenTrackingController.enabled = false;
            }
        }
    }

    tokenTrackingControllerHighlightedRangeWasClicked(tokenTrackingController)
    {
        console.assert(this._style.ownerRule.sourceCodeLocation);
        if (!this._style.ownerRule.sourceCodeLocation)
            return;

        // Special case command clicking url(...) links.
        var token = this._tokenTrackingController.candidate.hoveredToken;
        if (/\blink\b/.test(token.type)) {
            var url = token.string;
            var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url;
            WebInspector.openURL(absoluteURL(url, baseURL));
            return;
        }

        // Jump to the rule if we can't find a property.
        // Find a better source code location from the property that was clicked.
        var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation;
        var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start);
        for (var i = 0; i < marks.length; ++i) {
            var mark = marks[i];
            var property = mark.__cssProperty;
            if (property) {
                var sourceCode = sourceCodeLocation.sourceCode;
                var styleSheetTextRange = property.styleSheetTextRange;
                sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn);
            }
        }

        WebInspector.showSourceCodeLocation(sourceCodeLocation);
    }

    tokenTrackingControllerNewHighlightCandidate(tokenTrackingController, candidate)
    {
        this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange);
    }
};

WebInspector.CSSStyleDeclarationTextEditor.Event = {
    ContentChanged: "css-style-declaration-text-editor-content-changed",
    Blurred: "css-style-declaration-text-editor-blurred"
};

WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor";
WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only";
WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch";
WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName = "cubic-bezier-marker";
WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder";
WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line";
WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250;
WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000;
