summaryrefslogtreecommitdiff
path: root/Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
commit1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch)
tree46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js
parent32761a6cee1d0dee366b885b7b9c777e67885688 (diff)
downloadWebKitGtk-tarball-master.tar.gz
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js')
-rw-r--r--Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js1728
1 files changed, 1728 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js b/Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js
new file mode 100644
index 000000000..191749cc4
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js
@@ -0,0 +1,1728 @@
+/*
+ * 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.View
+{
+ constructor(delegate, style)
+ {
+ super();
+
+ 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._propertyVisibilityMode = WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.ShowAll;
+ this._showsImplicitProperties = true;
+ this._alwaysShowPropertyNames = {};
+ this._filterResultPropertyNames = null;
+ this._sortProperties = false;
+ this._hasActiveInlineSwatchEditor = false;
+
+ this._linePrefixWhitespace = "";
+
+ this._delegate = delegate || null;
+
+ this._codeMirror = WebInspector.CodeMirrorEditor.create(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;
+ this._shownProperties = [];
+ }
+
+ // Public
+
+ 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 shownProperties() { return this._shownProperties; }
+
+ 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 propertyVisibilityMode() { return this._propertyVisibilityMode; }
+ set propertyVisibilityMode(propertyVisibilityMode)
+ {
+ if (this._propertyVisibilityMode === propertyVisibilityMode)
+ return;
+
+ this._propertyVisibilityMode = propertyVisibilityMode;
+
+ 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();
+ }
+
+ 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.hasProperties())
+ 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
+
+ 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();
+ }
+
+ layout()
+ {
+ this._codeMirror.refresh();
+ }
+
+ // 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;
+ }
+
+ let cursor = codeMirror.getCursor();
+ let line = codeMirror.getLine(cursor.line);
+ let previousLine = codeMirror.getLine(cursor.line - 1);
+
+ if (!line && !previousLine && !cursor.line)
+ return switchRule.call(this);
+
+ let trimmedPreviousLine = previousLine ? previousLine.trimRight() : "";
+ let previousAnchor = 0;
+ let previousHead = line.length;
+ let isComment = this._textAtCursorIsComment(codeMirror, cursor);
+
+ if (cursor.ch === line.indexOf(":") || line.indexOf(":") < 0 || isComment) {
+ if (previousLine) {
+ --cursor.line;
+ previousHead = trimmedPreviousLine.length;
+
+ if (!this._textAtCursorIsComment(codeMirror, cursor)) {
+ let colon = /(?::\s*)/.exec(previousLine);
+ previousAnchor = colon ? colon.index + colon[0].length : 0;
+ if (trimmedPreviousLine.includes(";"))
+ previousHead = trimmedPreviousLine.lastIndexOf(";");
+ }
+
+ 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) {
+ let 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;
+ }
+
+ let cursor = codeMirror.getCursor();
+ let line = codeMirror.getLine(cursor.line);
+ let trimmedLine = line.trimRight();
+ let lastLine = cursor.line === codeMirror.lineCount() - 1;
+ let nextLine = codeMirror.getLine(cursor.line + 1);
+ let 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;
+ }
+
+ let hasEndingSemicolon = trimmedLine.endsWith(";");
+ let pastLastSemicolon = line.includes(";") && cursor.ch >= line.lastIndexOf(";");
+
+ if (cursor.ch >= line.trimRight().length - hasEndingSemicolon || pastLastSemicolon) {
+ this._completionController.completeAtCurrentPositionIfNeeded().then(function(result) {
+ if (result !== WebInspector.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound)
+ return;
+
+ let replacement = "";
+
+ if (!hasEndingSemicolon && !pastLastSemicolon && !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 = WebInspector.CSSStyleDeclarationTextEditor.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 += WebInspector.CSSStyleDeclarationTextEditor.SuffixWhitespace;
+
+ // This regular expression replacement removes extra newlines
+ // in between properties while preserving leading whitespace
+ return content.replace(/\s*\n\s*\n(\s*)/g, "\n$1");
+ }
+
+ _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._createInlineSwatches(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)
+ {
+ console.assert(!this._hasActiveInlineSwatchEditor, "We should never be recreating markers when we an active inline swatch editor.");
+
+ 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.
+ 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((lineHandler) => {
+ this._createCommentedCheckboxMarker(lineHandler);
+ });
+ }
+
+ // Look for swatchable values and make inline swatches.
+ this._createInlineSwatches(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; */
+ let commentedPropertyRegex = /\/\*\s*[-\w]+\s*\:\s*(?:(?:\".*\"|url\(.+\)|[^;])\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);
+ }
+ }
+
+ _createInlineSwatches(nonatomic, lineNumber)
+ {
+ function createSwatch(swatch, marker, valueObject, valueString)
+ {
+ swatch.addEventListener(WebInspector.InlineSwatch.Event.ValueChanged, this._inlineSwatchValueChanged, this);
+ swatch.addEventListener(WebInspector.InlineSwatch.Event.Activated, this._inlineSwatchActivated, this);
+ swatch.addEventListener(WebInspector.InlineSwatch.Event.Deactivated, this._inlineSwatchDeactivated, this);
+
+ let codeMirrorTextMarker = marker.codeMirrorTextMarker;
+ let codeMirrorTextMarkerRange = codeMirrorTextMarker.find();
+ this._codeMirror.setUniqueBookmark(codeMirrorTextMarkerRange.from, swatch.element);
+
+ swatch.__textMarker = codeMirrorTextMarker;
+ swatch.__textMarkerRange = codeMirrorTextMarkerRange;
+ }
+
+ function update()
+ {
+ let 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, (marker, color, colorString) => {
+ let swatch = new WebInspector.InlineSwatch(WebInspector.InlineSwatch.Type.Color, color, this._codeMirror.getOption("readOnly"));
+ createSwatch.call(this, swatch, marker, color, colorString);
+ });
+
+ // Look for gradient strings and add swatches in front of them.
+ createCodeMirrorGradientTextMarkers(this._codeMirror, range, (marker, gradient, gradientString) => {
+ let swatch = new WebInspector.InlineSwatch(WebInspector.InlineSwatch.Type.Gradient, gradient, this._codeMirror.getOption("readOnly"));
+ createSwatch.call(this, swatch, marker, gradient, gradientString);
+ });
+
+ // Look for cubic-bezier strings and add swatches in front of them.
+ createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range, (marker, bezier, bezierString) => {
+ let swatch = new WebInspector.InlineSwatch(WebInspector.InlineSwatch.Type.Bezier, bezier, this._codeMirror.getOption("readOnly"));
+ createSwatch.call(this, swatch, marker, bezier, bezierString);
+ });
+
+ // Look for spring strings and add swatches in front of them.
+ createCodeMirrorSpringTextMarkers(this._codeMirror, range, (marker, spring, springString) => {
+ let swatch = new WebInspector.InlineSwatch(WebInspector.InlineSwatch.Type.Spring, spring, this._codeMirror.getOption("readOnly"));
+ createSwatch.call(this, swatch, marker, spring, springString);
+ });
+
+ // Look for CSS variables and add swatches in front of them.
+ createCodeMirrorVariableTextMarkers(this._codeMirror, range, (marker, variable, variableString) => {
+ const dontCreateIfMissing = true;
+ let variableProperty = this._style.nodeStyles.computedStyle.propertyForName(variableString, dontCreateIfMissing);
+ if (!variableProperty)
+ return;
+
+ let trimmedValue = variableProperty.value.trim();
+ let swatch = new WebInspector.InlineSwatch(WebInspector.InlineSwatch.Type.Variable, trimmedValue, this._codeMirror.getOption("readOnly"));
+ createSwatch.call(this, swatch, marker, variableProperty, trimmedValue);
+ });
+ }
+
+ 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") {
+
+ let arrowElement = WebInspector.createGoToArrowButton();
+ arrowElement.title = WebInspector.UIString("Option-click to show source");
+
+ let delegate = this._delegate;
+ arrowElement.addEventListener("click", function(event) {
+ delegate.cssStyleDeclarationTextEditorShowProperty(property, event.altKey);
+ });
+
+ 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 (this._codeMirror.getOption("readOnly") || 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("Unsupported property “%s”").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)
+ {
+ let properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties;
+
+ let filterFunction = (property) => property; // Identity function.
+ if (this._filterResultPropertyNames) {
+ filterFunction = (property) => {
+ if (!property.variable && this._propertyVisibilityMode === WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.HideNonVariables)
+ return false;
+
+ if (property.variable && this._propertyVisibilityMode === WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.HideVariables)
+ return false;
+
+ if (property.implicit && !this._showsImplicitProperties)
+ return false;
+
+ if (!(property.name in this._filterResultPropertyNames))
+ return false;
+
+ return true;
+ };
+ } else if (!onlyVisibleProperties) {
+ // Filter based on options only when all properties are used.
+ filterFunction = (property) => {
+ switch (this._propertyVisibilityMode) {
+ case WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.HideNonVariables:
+ if (!property.variable)
+ return false;
+
+ break;
+ case WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.HideVariables:
+ if (property.variable)
+ return false;
+
+ break;
+
+ case WebInspector.CSSStyleDeclarationTextEditor.PropertyVisibilityMode.ShowAll:
+ break;
+
+ default:
+ console.error("Invalid property visibility mode");
+ break;
+ }
+
+ return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames;
+ };
+ }
+
+ properties = properties.filter(filterFunction);
+ if (this._sortProperties)
+ properties.sort((a, b) => a.name.localeCompare(b.name));
+
+ this._shownProperties = properties;
+
+ 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 inline swatches that got removed.
+ this._createInlineSwatches(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 inline swatches that got removed.
+ this._createInlineSwatches(true, range.from.line);
+ }
+
+ this._codeMirror.operation(update.bind(this));
+ }
+
+ _inlineSwatchValueChanged(event)
+ {
+ let swatch = event && event.target;
+ console.assert(swatch);
+ if (!swatch)
+ return;
+
+ let value = event.data && event.data.value && event.data.value.toString();
+ console.assert(value);
+ if (!value)
+ return;
+
+ let textMarker = swatch.__textMarker;
+ let range = swatch.__textMarkerRange;
+ console.assert(range);
+ if (!range)
+ return;
+
+ function update()
+ {
+ // Sometimes we still might find a stale text marker with findMarksAt.
+ range = textMarker.find();
+ if (!range)
+ return;
+
+ textMarker.clear();
+
+ this._codeMirror.replaceRange(value, range.from, range.to);
+
+ // The value's text could have changed, so we need to update the "range"
+ // variable to anticipate a different "range.to" property.
+ range.to.ch = range.from.ch + value.length;
+
+ textMarker = this._codeMirror.markText(range.from, range.to);
+
+ swatch.__textMarker = textMarker;
+ }
+
+ this._codeMirror.operation(update.bind(this));
+ }
+
+ _inlineSwatchActivated()
+ {
+ this._hasActiveInlineSwatchEditor = true;
+ }
+
+ _inlineSwatchDeactivated()
+ {
+ this._hasActiveInlineSwatchEditor = false;
+ }
+
+ _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;
+
+ if (this._hasActiveInlineSwatchEditor)
+ return;
+
+ // Don't try to update the document after just modifying a swatch.
+ if (this._ignoreNextPropertiesChanged) {
+ this._ignoreNextPropertiesChanged = false;
+ 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()
+ {
+ let indentString = WebInspector.indentString();
+ let builder = new FormatterContentBuilder(indentString);
+ let formatter = new WebInspector.Formatter(this._codeMirror, builder);
+ let start = {line: 0, ch: 0};
+ let 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;
+
+ this._linePrefixWhitespace = WebInspector.indentString();
+
+ 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 swatchable values and make inline swatches.
+ this._createInlineSwatches(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)
+ {
+ let sourceCodeLocation = this._style.ownerRule.sourceCodeLocation;
+ console.assert(sourceCodeLocation);
+ if (!sourceCodeLocation)
+ return;
+
+ let candidate = tokenTrackingController.candidate;
+ console.assert(candidate);
+ if (!candidate)
+ return;
+
+ let token = candidate.hoveredToken;
+
+ // Special case command clicking url(...) links.
+ if (token && /\blink\b/.test(token.type)) {
+ let url = token.string;
+ let baseURL = sourceCodeLocation.sourceCode.url;
+ WebInspector.openURL(absoluteURL(url, baseURL));
+ return;
+ }
+
+ function showRangeInSourceCode(sourceCode, range)
+ {
+ if (!sourceCode || !range)
+ return false;
+
+ WebInspector.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn));
+ return true;
+ }
+
+ // Special case option clicking CSS variables.
+ if (token && /\bvariable-2\b/.test(token.type)) {
+ let property = this._style.nodeStyles.effectivePropertyForName(token.string);
+ if (property && showRangeInSourceCode(property.ownerStyle.ownerRule.sourceCodeLocation.sourceCode, property.styleSheetTextRange))
+ return;
+ }
+
+ // Jump to the rule if we can't find a property.
+ // Find a better source code location from the property that was clicked.
+ let marks = this._codeMirror.findMarksAt(candidate.hoveredTokenRange.start);
+ for (let mark of marks) {
+ let property = mark.__cssProperty;
+ if (property && showRangeInSourceCode(sourceCodeLocation.sourceCode, property.styleSheetTextRange))
+ return;
+ }
+ }
+
+ 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.PropertyVisibilityMode = {
+ ShowAll: Symbol("variable-visibility-show-all"),
+ HideVariables: Symbol("variable-visibility-hide-variables"),
+ HideNonVariables: Symbol("variable-visibility-hide-non-variables"),
+};
+
+WebInspector.CSSStyleDeclarationTextEditor.PrefixWhitespace = "\n";
+WebInspector.CSSStyleDeclarationTextEditor.SuffixWhitespace = "\n";
+WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor";
+WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only";
+WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder";
+WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line";
+WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250;
+WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000;