/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * 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 = function(delegate, style, element) { WebInspector.Object.call(this); this._element = element || document.createElement("div"); this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName); this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName); this._showsImplicitProperties = true; this._alwaysShowPropertyNames = {}; this._sortProperties = false; this._prefixWhitespace = ""; this._suffixWhitespace = ""; this._linePrefixWhitespace = ""; this._delegate = delegate || null; this._codeMirror = CodeMirror(this.element, { readOnly: true, lineWrapping: true, mode: "css-rule", electricChars: false, indentWithTabs: true, indentUnit: 4, smartIndent: false, matchBrackets: true, autoCloseBrackets: true }); this._codeMirror.on("change", this._contentChanged.bind(this)); this._codeMirror.on("blur", this._editorBlured.bind(this)); this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this); this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this); this._jumpToSymbolTrackingModeEnabled = false; this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName; this._tokenTrackingController.mouseOverDelayDuration = 0; this._tokenTrackingController.mouseOutReleaseDelayDuration = 0; this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens; this.style = style; }; WebInspector.Object.addConstructorFunctions(WebInspector.CSSStyleDeclarationTextEditor); WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor"; WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only"; WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch"; WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder"; WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line"; WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250; WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000; WebInspector.CSSStyleDeclarationTextEditor.prototype = { constructor: WebInspector.CSSStyleDeclarationTextEditor, // 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: function() { this._codeMirror.focus(); }, refresh: function() { this._resetContent(); }, updateLayout: function(force) { this._codeMirror.refresh(); }, // Protected didDismissPopover: function(popover) { if (popover === this._colorPickerPopover) delete this._colorPickerPopover; }, completionControllerCompletionsHidden: function(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 _clearRemoveEditingLineClassesTimeout: function() { if (!this._removeEditingLineClassesTimeout) return; clearTimeout(this._removeEditingLineClassesTimeout); delete this._removeEditingLineClassesTimeout; }, _removeEditingLineClasses: function() { 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: function() { if (this._removeEditingLineClassesTimeout) return; this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay); }, _formattedContent: function() { // 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: function() { if (this._commitChangesTimeout) { clearTimeout(this._commitChangesTimeout); delete this._commitChangesTimeout; } this._style.text = this._formattedContent(); }, _editorBlured: function(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(); }, _contentChanged: function(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); // 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. const 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); }, _updateTextMarkers: function(nonatomic) { function update() { this._clearTextMarkers(true); var styleText = this._style.text; 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")) { // Matches a comment like: /* -webkit-foo: bar; */ const commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g; // Look for comments that look like properties and add checkboxes in front of them. var lineCount = this._codeMirror.lineCount(); for (var i = 0; i < lineCount; ++i) { var lineContent = this._codeMirror.getLine(i); var match = commentedPropertyRegex.exec(lineContent); while (match) { var checkboxElement = document.createElement("input"); checkboxElement.type = "checkbox"; checkboxElement.checked = false; checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this)); var from = {line: i, ch: match.index}; var to = {line: i, 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(lineContent); } } } // Look for colors and make swatches. this._createColorSwatches(true); this._markLinesWithCheckboxPlaceholder(); } if (nonatomic) update.call(this); else this._codeMirror.operation(update.bind(this)); }, _createColorSwatches: function(nonatomic, lineNumber) { function update() { // Look for color strings and add swatches in front of them. this._codeMirror.createColorMarkers(lineNumber, function(marker, color, colorString) { var swatchElement = document.createElement("span"); swatchElement.title = WebInspector.UIString("Click to open a colorpicker. Shift-click to change color format."); 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; var swatchMarker = 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)); }, _updateTextMarkerForPropertyIfNeeded: function(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: function(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; } 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) classNames.push("invalid"); if (!property.enabled) classNames.push("disabled"); 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); }, _clearTextMarkers: function(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: function(onlyVisibleProperties, callback) { var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties; 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; }).bind(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: function(event) { var property = event.target.__cssProperty; console.assert(property); if (!property) return; 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; 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 that got removed. this._createColorSwatches(true, range.from.line); } this._codeMirror.operation(update.bind(this)); }, _propertyCommentCheckboxChanged: function(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; 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 that got removed. this._createColorSwatches(true, range.from.line); } this._codeMirror.operation(update.bind(this)); }, _colorSwatchClicked: function(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 (!mark.__markedColor) 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); colorTextMarker.__markedColor = true; swatch.__colorTextMarker = colorTextMarker; } this._codeMirror.operation(update.bind(this)); } if (event.shiftKey) { 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; } }, _propertyOverriddenStatusChanged: function(event) { this._updateTextMarkerForPropertyIfNeeded(event.target); }, _propertiesChanged: function(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._formattedContent()) { this._resetContent(); return; } this._removeEditingLineClassesSoon(); this._updateTextMarkers(); }, _markLinesWithCheckboxPlaceholder: function() { 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: function(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; } }, _resetContent: function() { if (this._commitChangesTimeout) { clearTimeout(this._commitChangesTimeout); delete this._commitChangesTimeout; } this._removeEditingLineClasses(); // Only allow editing if we have a style, it is editable and we have text range in the stylesheet. var 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(); delete this._ignoreCodeMirrorContentDidChangeEvent; return; } function update() { // Remember the cursor position/selection. var selectionAnchor = this._codeMirror.getCursor("anchor"); var selectionHead = this._codeMirror.getCursor("head"); function countNewLineCharacters(text) { var matches = text.match(/\n/g); return matches ? matches.length : 0; } var styleText = this._style.text; // Pretty print the content if there are more properties than there are lines. // This could be an option exposed to the user; however, it is almost always // desired in this case. if (styleText && this._style.visibleProperties.length <= countNewLineCharacters(styleText.trim()) + 1) { // This style has formatted text content, so use it for a high-fidelity experience. var prefixWhitespaceMatch = styleText.match(/^[ \t]*\n/); this._prefixWhitespace = prefixWhitespaceMatch ? prefixWhitespaceMatch[0] : ""; var suffixWhitespaceMatch = styleText.match(/\n[ \t]*$/); this._suffixWhitespace = suffixWhitespaceMatch ? suffixWhitespaceMatch[0] : ""; this._codeMirror.setValue(styleText); if (this._prefixWhitespace) this._codeMirror.removeLine(0); if (this._suffixWhitespace) { var lineCount = this._codeMirror.lineCount(); this._codeMirror.replaceRange("", {line: lineCount - 2}, {line: lineCount - 1}); } this._linePrefixWhitespace = ""; var linesToStrip = []; // Remember the whitespace so it can be restored on commit. var lineCount = this._codeMirror.lineCount(); for (var i = 0; i < lineCount; ++i) { var lineContent = this._codeMirror.getLine(i); var prefixWhitespaceMatch = lineContent.match(/^\s+/); if (!prefixWhitespaceMatch) continue; linesToStrip.push(i); // Only remember the shortest whitespace so we don't loose any of the // original author's whitespace if their indentation lengths differed. // Using the shortest also makes the adjustment work in _updateTextMarkers. // FIXME: This messes up if there is a mix of spaces and tabs. One tab // will be shorter than 4 or 8 spaces, but will look the same visually. if (!this._linePrefixWhitespace || prefixWhitespaceMatch[0].length < this._linePrefixWhitespace.length) this._linePrefixWhitespace = prefixWhitespaceMatch[0]; } // Strip the whitespace from the beginning of each line. for (var i = 0; i < linesToStrip.length; ++i) { var lineNumber = linesToStrip[i]; var from = {line: lineNumber, ch: 0}; var to = {line: lineNumber, ch: this._linePrefixWhitespace.length}; this._codeMirror.replaceRange("", from, to); } // Update all the text markers. this._updateTextMarkers(true); } else { // This style does not have text content or it is minified, so we want to synthesize the text content. this._prefixWhitespace = ""; this._suffixWhitespace = ""; this._linePrefixWhitespace = ""; this._codeMirror.setValue(""); var lineNumber = 0; // Iterate only visible properties if we have original style text. That way we known we only syntesize // what was originaly in the style text. this._iterateOverProperties(styleText ? true : false, function(property) { // Some property text can have line breaks, so consider that in the ranges below. var propertyText = property.synthesizedText; var propertyLineCount = countNewLineCharacters(propertyText); var from = {line: lineNumber, ch: 0}; var to = {line: lineNumber + propertyLineCount}; this._codeMirror.replaceRange((lineNumber ? "\n" : "") + propertyText, from); this._createTextMarkerForPropertyIfNeeded(from, to, property); lineNumber += propertyLineCount + 1; }); // Look for colors and make swatches. this._createColorSwatches(true); } this._markLinesWithCheckboxPlaceholder(); // 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 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)); delete this._ignoreCodeMirrorContentDidChangeEvent; }, _updateJumpToSymbolTrackingMode: function() { var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled; if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation) this._jumpToSymbolTrackingModeEnabled = false; else this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.altKey && !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: function(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.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation); }, tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate) { this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange); } }; WebInspector.CSSStyleDeclarationTextEditor.prototype.__proto__ = WebInspector.Object.prototype;