blob: fea8b45c8ccf1cb8a165aa60cbccccc6e9a606c5 [file] [log] [blame]
/*
* Copyright (C) 2013 Google 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
* OWNER OR 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.
*/
Sources.CSSPlugin = class extends Sources.UISourceCodeFrame.Plugin {
/**
* @param {!SourceFrame.SourcesTextEditor} textEditor
*/
constructor(textEditor) {
super();
this._textEditor = textEditor;
this._swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper();
this._muteSwatchProcessing = false;
this._hadSwatchChange = false;
/** @type {?InlineEditor.BezierEditor} */
this._bezierEditor = null;
/** @type {?TextUtils.TextRange} */
this._editedSwatchTextRange = null;
/** @type {?ColorPicker.Spectrum} */
this._spectrum = null;
/** @type {?Element} */
this._currentSwatch = null;
this._textEditor.configureAutocomplete(
{suggestionsCallback: this._cssSuggestions.bind(this), isWordChar: this._isWordChar.bind(this)});
this._textEditor.addEventListener(
SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._textEditorScrolled, this);
this._textEditor.addEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this);
this._updateSwatches(0, this._textEditor.linesCount - 1);
this._shortcuts = {};
this._registerShortcuts();
this._boundHandleKeyDown = this._handleKeyDown.bind(this);
this._textEditor.element.addEventListener('keydown', this._boundHandleKeyDown, false);
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
static accepts(uiSourceCode) {
return uiSourceCode.contentType().isStyleSheet();
}
_registerShortcuts() {
const shortcutKeys = UI.ShortcutsScreen.SourcesPanelShortcuts;
for (const descriptor of shortcutKeys.IncreaseCSSUnitByOne)
this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, 1);
for (const descriptor of shortcutKeys.DecreaseCSSUnitByOne)
this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, -1);
for (const descriptor of shortcutKeys.IncreaseCSSUnitByTen)
this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, 10);
for (const descriptor of shortcutKeys.DecreaseCSSUnitByTen)
this._shortcuts[descriptor.key] = this._handleUnitModification.bind(this, -10);
}
/**
* @param {!Event} event
*/
_handleKeyDown(event) {
const shortcutKey = UI.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
const handler = this._shortcuts[shortcutKey];
if (handler && handler())
event.consume(true);
}
_textEditorScrolled() {
if (this._swatchPopoverHelper.isShowing())
this._swatchPopoverHelper.hide(true);
}
/**
* @param {string} unit
* @param {number} change
* @return {?string}
*/
_modifyUnit(unit, change) {
const unitValue = parseInt(unit, 10);
if (isNaN(unitValue))
return null;
const tail = unit.substring((unitValue).toString().length);
return String.sprintf('%d%s', unitValue + change, tail);
}
/**
* @param {number} change
* @return {boolean}
*/
_handleUnitModification(change) {
const selection = this._textEditor.selection().normalize();
let token = this._textEditor.tokenAtTextPosition(selection.startLine, selection.startColumn);
if (!token) {
if (selection.startColumn > 0)
token = this._textEditor.tokenAtTextPosition(selection.startLine, selection.startColumn - 1);
if (!token)
return false;
}
if (token.type !== 'css-number')
return false;
const cssUnitRange =
new TextUtils.TextRange(selection.startLine, token.startColumn, selection.startLine, token.endColumn);
const cssUnitText = this._textEditor.text(cssUnitRange);
const newUnitText = this._modifyUnit(cssUnitText, change);
if (!newUnitText)
return false;
this._textEditor.editRange(cssUnitRange, newUnitText);
selection.startColumn = token.startColumn;
selection.endColumn = selection.startColumn + newUnitText.length;
this._textEditor.setSelection(selection);
return true;
}
/**
* @param {number} startLine
* @param {number} endLine
*/
_updateSwatches(startLine, endLine) {
const swatches = [];
const swatchPositions = [];
const regexes =
[SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex, UI.Geometry.CubicBezier.Regex, Common.Color.Regex];
const handlers = new Map();
handlers.set(Common.Color.Regex, this._createColorSwatch.bind(this));
handlers.set(UI.Geometry.CubicBezier.Regex, this._createBezierSwatch.bind(this));
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
const line = this._textEditor.line(lineNumber).substring(0, Sources.CSSPlugin.maxSwatchProcessingLength);
const results = TextUtils.TextUtils.splitStringByRegexes(line, regexes);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.regexIndex === -1 || !handlers.has(regexes[result.regexIndex]))
continue;
const delimiters = /[\s:;,(){}]/;
const positionBefore = result.position - 1;
const positionAfter = result.position + result.value.length;
if (positionBefore >= 0 && !delimiters.test(line.charAt(positionBefore)) ||
positionAfter < line.length && !delimiters.test(line.charAt(positionAfter)))
continue;
const swatch = handlers.get(regexes[result.regexIndex])(result.value);
if (!swatch)
continue;
swatches.push(swatch);
swatchPositions.push(TextUtils.TextRange.createFromLocation(lineNumber, result.position));
}
}
this._textEditor.operation(putSwatchesInline.bind(this));
/**
* @this {Sources.CSSPlugin}
*/
function putSwatchesInline() {
const clearRange = new TextUtils.TextRange(startLine, 0, endLine, this._textEditor.line(endLine).length);
this._textEditor.bookmarks(clearRange, Sources.CSSPlugin.SwatchBookmark).forEach(marker => marker.clear());
for (let i = 0; i < swatches.length; i++) {
const swatch = swatches[i];
const swatchPosition = swatchPositions[i];
const bookmark = this._textEditor.addBookmark(
swatchPosition.startLine, swatchPosition.startColumn, swatch, Sources.CSSPlugin.SwatchBookmark);
swatch[Sources.CSSPlugin.SwatchBookmark] = bookmark;
}
}
}
/**
* @param {string} text
* @return {?InlineEditor.ColorSwatch}
*/
_createColorSwatch(text) {
const color = Common.Color.parse(text);
if (!color)
return null;
const swatch = InlineEditor.ColorSwatch.create();
swatch.setColor(color);
swatch.iconElement().title = Common.UIString('Open color picker.');
swatch.iconElement().addEventListener('click', this._swatchIconClicked.bind(this, swatch), false);
swatch.hideText(true);
return swatch;
}
/**
* @param {string} text
* @return {?InlineEditor.BezierSwatch}
*/
_createBezierSwatch(text) {
if (!UI.Geometry.CubicBezier.parse(text))
return null;
const swatch = InlineEditor.BezierSwatch.create();
swatch.setBezierText(text);
swatch.iconElement().title = Common.UIString('Open cubic bezier editor.');
swatch.iconElement().addEventListener('click', this._swatchIconClicked.bind(this, swatch), false);
swatch.hideText(true);
return swatch;
}
/**
* @param {!Element} swatch
* @param {!Event} event
*/
_swatchIconClicked(swatch, event) {
event.consume(true);
this._hadSwatchChange = false;
this._muteSwatchProcessing = true;
const swatchPosition = swatch[Sources.CSSPlugin.SwatchBookmark].position();
this._textEditor.setSelection(swatchPosition);
this._editedSwatchTextRange = swatchPosition.clone();
this._editedSwatchTextRange.endColumn += swatch.textContent.length;
this._currentSwatch = swatch;
if (swatch instanceof InlineEditor.ColorSwatch)
this._showSpectrum(swatch);
else if (swatch instanceof InlineEditor.BezierSwatch)
this._showBezierEditor(swatch);
}
/**
* @param {!InlineEditor.ColorSwatch} swatch
*/
_showSpectrum(swatch) {
if (!this._spectrum) {
this._spectrum = new ColorPicker.Spectrum();
this._spectrum.addEventListener(ColorPicker.Spectrum.Events.SizeChanged, this._spectrumResized, this);
this._spectrum.addEventListener(ColorPicker.Spectrum.Events.ColorChanged, this._spectrumChanged, this);
}
this._spectrum.setColor(swatch.color(), swatch.format());
this._swatchPopoverHelper.show(this._spectrum, swatch.iconElement(), this._swatchPopoverHidden.bind(this));
}
/**
* @param {!Common.Event} event
*/
_spectrumResized(event) {
this._swatchPopoverHelper.reposition();
}
/**
* @param {!Common.Event} event
*/
_spectrumChanged(event) {
const colorString = /** @type {string} */ (event.data);
const color = Common.Color.parse(colorString);
if (!color)
return;
this._currentSwatch.setColor(color);
this._changeSwatchText(colorString);
}
/**
* @param {!InlineEditor.BezierSwatch} swatch
*/
_showBezierEditor(swatch) {
if (!this._bezierEditor) {
this._bezierEditor = new InlineEditor.BezierEditor();
this._bezierEditor.addEventListener(InlineEditor.BezierEditor.Events.BezierChanged, this._bezierChanged, this);
}
let cubicBezier = UI.Geometry.CubicBezier.parse(swatch.bezierText());
if (!cubicBezier) {
cubicBezier =
/** @type {!UI.Geometry.CubicBezier} */ (UI.Geometry.CubicBezier.parse('linear'));
}
this._bezierEditor.setBezier(cubicBezier);
this._swatchPopoverHelper.show(this._bezierEditor, swatch.iconElement(), this._swatchPopoverHidden.bind(this));
}
/**
* @param {!Common.Event} event
*/
_bezierChanged(event) {
const bezierString = /** @type {string} */ (event.data);
this._currentSwatch.setBezierText(bezierString);
this._changeSwatchText(bezierString);
}
/**
* @param {string} text
*/
_changeSwatchText(text) {
this._hadSwatchChange = true;
this._textEditor.editRange(
/** @type {!TextUtils.TextRange} */ (this._editedSwatchTextRange), text, '*swatch-text-changed');
this._editedSwatchTextRange.endColumn = this._editedSwatchTextRange.startColumn + text.length;
}
/**
* @param {boolean} commitEdit
*/
_swatchPopoverHidden(commitEdit) {
this._muteSwatchProcessing = false;
if (!commitEdit && this._hadSwatchChange)
this._textEditor.undo();
}
/**
* @param {!Common.Event} event
*/
_onTextChanged(event) {
if (!this._muteSwatchProcessing)
this._updateSwatches(event.data.newRange.startLine, event.data.newRange.endLine);
}
/**
* @param {string} char
* @return {boolean}
*/
_isWordChar(char) {
return TextUtils.TextUtils.isWordChar(char) || char === '.' || char === '-' || char === '$';
}
/**
* @param {!TextUtils.TextRange} prefixRange
* @param {!TextUtils.TextRange} substituteRange
* @return {?Promise.<!UI.SuggestBox.Suggestions>}
*/
_cssSuggestions(prefixRange, substituteRange) {
const prefix = this._textEditor.text(prefixRange);
if (prefix.startsWith('$'))
return null;
const propertyToken = this._backtrackPropertyToken(prefixRange.startLine, prefixRange.startColumn - 1);
if (!propertyToken)
return null;
const line = this._textEditor.line(prefixRange.startLine);
const tokenContent = line.substring(propertyToken.startColumn, propertyToken.endColumn);
const propertyValues = SDK.cssMetadata().propertyValues(tokenContent);
return Promise.resolve(propertyValues.filter(value => value.startsWith(prefix)).map(value => ({text: value})));
}
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?{startColumn: number, endColumn: number, type: string}}
*/
_backtrackPropertyToken(lineNumber, columnNumber) {
const backtrackDepth = 10;
let tokenPosition = columnNumber;
const line = this._textEditor.line(lineNumber);
let seenColon = false;
for (let i = 0; i < backtrackDepth && tokenPosition >= 0; ++i) {
const token = this._textEditor.tokenAtTextPosition(lineNumber, tokenPosition);
if (!token)
return null;
if (token.type === 'css-property')
return seenColon ? token : null;
if (token.type && !(token.type.indexOf('whitespace') !== -1 || token.type.startsWith('css-comment')))
return null;
if (!token.type && line.substring(token.startColumn, token.endColumn) === ':') {
if (!seenColon)
seenColon = true;
else
return null;
}
tokenPosition = token.startColumn - 1;
}
return null;
}
/**
* @override
*/
dispose() {
if (this._swatchPopoverHelper.isShowing())
this._swatchPopoverHelper.hide(true);
this._textEditor.removeEventListener(
SourceFrame.SourcesTextEditor.Events.ScrollChanged, this._textEditorScrolled, this);
this._textEditor.removeEventListener(UI.TextEditor.Events.TextChanged, this._onTextChanged, this);
this._textEditor.bookmarks(this._textEditor.fullRange(), Sources.CSSPlugin.SwatchBookmark)
.forEach(marker => marker.clear());
this._textEditor.element.removeEventListener('keydown', this._boundHandleKeyDown, false);
}
};
/** @type {number} */
Sources.CSSPlugin.maxSwatchProcessingLength = 300;
/** @type {symbol} */
Sources.CSSPlugin.SwatchBookmark = Symbol('swatch');