blob: fc5e3ef707a7b0799de51b9a5d775270a0051777 [file] [log] [blame]
/*
* 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;