mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 21:28:43 +08:00
- Migrate sources to use Localization V2 APIs. - Removed sources grdps Tested with en-XL.json https://imgur.com/a/vDrfTAv Bug: 1136655 Change-Id: I01d4fcdc3f115e9edb3fae933c7d5c6b9ffb77c0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2692743 Commit-Queue: Vidal Diazleal <vidorteg@microsoft.com> Reviewed-by: Peter Marshall <petermarshall@chromium.org>
482 lines
17 KiB
JavaScript
482 lines
17 KiB
JavaScript
/*
|
|
* 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.
|
|
*/
|
|
|
|
import * as ColorPicker from '../color_picker/color_picker.js';
|
|
import * as Common from '../common/common.js';
|
|
import * as i18n from '../i18n/i18n.js';
|
|
import * as InlineEditor from '../inline_editor/inline_editor.js';
|
|
import * as Platform from '../platform/platform.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
import * as SourceFrame from '../source_frame/source_frame.js';
|
|
import * as TextEditor from '../text_editor/text_editor.js'; // eslint-disable-line no-unused-vars
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars
|
|
|
|
import {Plugin} from './Plugin.js';
|
|
|
|
export const UIStrings = {
|
|
/**
|
|
*@description Swatch icon element title in CSSPlugin of the Sources panel
|
|
*/
|
|
openColorPicker: 'Open color picker.',
|
|
/**
|
|
*@description Text to open the cubic bezier editor
|
|
*/
|
|
openCubicBezierEditor: 'Open cubic bezier editor.',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('sources/CSSPlugin.js', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
export class CSSPlugin extends Plugin {
|
|
/**
|
|
* @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor
|
|
*/
|
|
constructor(textEditor) {
|
|
super();
|
|
this._textEditor = textEditor;
|
|
this._swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper();
|
|
this._muteSwatchProcessing = false;
|
|
this._hadSwatchChange = false;
|
|
/** @type {?InlineEditor.BezierEditor.BezierEditor} */
|
|
this._bezierEditor = null;
|
|
/** @type {?TextUtils.TextRange.TextRange} */
|
|
this._editedSwatchTextRange = null;
|
|
/** @type {?ColorPicker.Spectrum.Spectrum} */
|
|
this._spectrum = null;
|
|
/** @type {?Element} */
|
|
this._currentSwatch = null;
|
|
this._textEditor.configureAutocomplete({
|
|
suggestionsCallback: this._cssSuggestions.bind(this),
|
|
isWordChar: this._isWordChar.bind(this),
|
|
anchorBehavior: undefined,
|
|
substituteRangeCallback: undefined,
|
|
tooltipCallback: undefined
|
|
});
|
|
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._boundHandleKeyDown = null;
|
|
|
|
this._registerShortcuts();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
|
|
* @return {boolean}
|
|
*/
|
|
static accepts(uiSourceCode) {
|
|
return uiSourceCode.contentType().isStyleSheet();
|
|
}
|
|
|
|
_registerShortcuts() {
|
|
this._boundHandleKeyDown =
|
|
UI.ShortcutRegistry.ShortcutRegistry.instance().addShortcutListener(this._textEditor.element, {
|
|
'sources.increment-css': this._handleUnitModification.bind(this, 1),
|
|
'sources.increment-css-by-ten': this._handleUnitModification.bind(this, 10),
|
|
'sources.decrement-css': this._handleUnitModification.bind(this, -1),
|
|
'sources.decrement-css-by-ten': this._handleUnitModification.bind(this, -10)
|
|
});
|
|
}
|
|
|
|
_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 Platform.StringUtilities.sprintf('%d%s', unitValue + change, tail);
|
|
}
|
|
|
|
/**
|
|
* @param {number} change
|
|
* @return {!Promise.<boolean>}
|
|
*/
|
|
async _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.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) {
|
|
/** @type {!Array<!HTMLElement>} */
|
|
const swatches = [];
|
|
/** @type {!Array<!TextUtils.TextRange.TextRange>} */
|
|
const swatchPositions = [];
|
|
|
|
const regexes =
|
|
[SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex, UI.Geometry.CubicBezier.Regex, Common.Color.Regex];
|
|
/** @type {!Map<!RegExp, function(string): ?HTMLElement>} */
|
|
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, maxSwatchProcessingLength);
|
|
const results = TextUtils.TextUtils.Utils.splitStringByRegexes(line, regexes);
|
|
for (let i = 0; i < results.length; i++) {
|
|
const result = results[i];
|
|
const handler = handlers.get(regexes[result.regexIndex]);
|
|
if (result.regexIndex === -1 || !handler) {
|
|
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 = handler(result.value);
|
|
if (!swatch) {
|
|
continue;
|
|
}
|
|
swatches.push(swatch);
|
|
swatchPositions.push(TextUtils.TextRange.TextRange.createFromLocation(lineNumber, result.position));
|
|
}
|
|
}
|
|
this._textEditor.operation(putSwatchesInline.bind(this));
|
|
|
|
/**
|
|
* @this {CSSPlugin}
|
|
*/
|
|
function putSwatchesInline() {
|
|
const clearRange =
|
|
new TextUtils.TextRange.TextRange(startLine, 0, endLine, this._textEditor.line(endLine).length);
|
|
this._textEditor.bookmarks(clearRange, 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, SwatchBookmark);
|
|
swatchToBookmark.set(swatch, bookmark);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {?HTMLElement}
|
|
*/
|
|
_createColorSwatch(text) {
|
|
const color = Common.Color.Color.parse(text);
|
|
if (!color) {
|
|
return null;
|
|
}
|
|
const swatch = new InlineEditor.ColorSwatch.ColorSwatch();
|
|
swatch.renderColor(color, false, i18nString(UIStrings.openColorPicker));
|
|
const value = swatch.createChild('span');
|
|
value.textContent = text;
|
|
value.setAttribute('hidden', 'true');
|
|
|
|
swatch.addEventListener('swatch-click', this._swatchIconClicked.bind(this, swatch), false);
|
|
return swatch;
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {?InlineEditor.Swatches.BezierSwatch}
|
|
*/
|
|
_createBezierSwatch(text) {
|
|
if (!UI.Geometry.CubicBezier.parse(text)) {
|
|
return null;
|
|
}
|
|
const swatch = InlineEditor.Swatches.BezierSwatch.create();
|
|
swatch.setBezierText(text);
|
|
UI.Tooltip.Tooltip.install(swatch.iconElement(), i18nString(UIStrings.openCubicBezierEditor));
|
|
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 bookmark = swatchToBookmark.get(swatch);
|
|
if (!bookmark) {
|
|
return;
|
|
}
|
|
const swatchPosition = bookmark.position();
|
|
if (!swatchPosition) {
|
|
return;
|
|
}
|
|
this._textEditor.setSelection(swatchPosition);
|
|
this._editedSwatchTextRange = swatchPosition.clone();
|
|
if (this._editedSwatchTextRange) {
|
|
this._editedSwatchTextRange.endColumn += (swatch.textContent || '').length;
|
|
}
|
|
this._currentSwatch = swatch;
|
|
|
|
if (InlineEditor.ColorSwatch.ColorSwatch.isColorSwatch(swatch)) {
|
|
this._showSpectrum(/** @type {!InlineEditor.ColorSwatch.ColorSwatch} */ (swatch));
|
|
} else if (swatch instanceof InlineEditor.Swatches.BezierSwatch) {
|
|
this._showBezierEditor(swatch);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!InlineEditor.ColorSwatch.ColorSwatch} swatch
|
|
*/
|
|
_showSpectrum(swatch) {
|
|
if (!this._spectrum) {
|
|
this._spectrum = new ColorPicker.Spectrum.Spectrum();
|
|
this._spectrum.addEventListener(ColorPicker.Spectrum.Events.SizeChanged, this._spectrumResized, this);
|
|
this._spectrum.addEventListener(ColorPicker.Spectrum.Events.ColorChanged, this._spectrumChanged, this);
|
|
}
|
|
this._spectrum.setColor(/** @type {!Common.Color.Color} */ (swatch.getColor()), swatch.getFormat() || '');
|
|
this._swatchPopoverHelper.show(this._spectrum, swatch, this._swatchPopoverHidden.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_spectrumResized(event) {
|
|
this._swatchPopoverHelper.reposition();
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_spectrumChanged(event) {
|
|
const colorString = /** @type {string} */ (event.data);
|
|
const color = Common.Color.Color.parse(colorString);
|
|
if (!color || !this._currentSwatch) {
|
|
return;
|
|
}
|
|
|
|
if (InlineEditor.ColorSwatch.ColorSwatch.isColorSwatch(this._currentSwatch)) {
|
|
const swatch = /** @type {!InlineEditor.ColorSwatch.ColorSwatch} */ (this._currentSwatch);
|
|
swatch.renderColor(color);
|
|
}
|
|
this._changeSwatchText(colorString);
|
|
}
|
|
|
|
/**
|
|
* @param {!InlineEditor.Swatches.BezierSwatch} swatch
|
|
*/
|
|
_showBezierEditor(swatch) {
|
|
const cubicBezier = UI.Geometry.CubicBezier.parse(swatch.bezierText()) ||
|
|
/** @type {!UI.Geometry.CubicBezier} */ (UI.Geometry.CubicBezier.parse('linear'));
|
|
if (!this._bezierEditor) {
|
|
this._bezierEditor = new InlineEditor.BezierEditor.BezierEditor(cubicBezier);
|
|
this._bezierEditor.addEventListener(InlineEditor.BezierEditor.Events.BezierChanged, this._bezierChanged, this);
|
|
} else {
|
|
this._bezierEditor.setBezier(cubicBezier);
|
|
}
|
|
this._swatchPopoverHelper.show(this._bezierEditor, swatch.iconElement(), this._swatchPopoverHidden.bind(this));
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_bezierChanged(event) {
|
|
const bezierString = /** @type {string} */ (event.data);
|
|
if (this._currentSwatch instanceof InlineEditor.Swatches.BezierSwatch) {
|
|
this._currentSwatch.setBezierText(bezierString);
|
|
}
|
|
this._changeSwatchText(bezierString);
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
*/
|
|
_changeSwatchText(text) {
|
|
this._hadSwatchChange = true;
|
|
const editedRange = /** @type {!TextUtils.TextRange.TextRange} */ (this._editedSwatchTextRange);
|
|
this._textEditor.editRange(editedRange, text, '*swatch-text-changed');
|
|
editedRange.endColumn = editedRange.startColumn + text.length;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} commitEdit
|
|
*/
|
|
_swatchPopoverHidden(commitEdit) {
|
|
this._muteSwatchProcessing = false;
|
|
if (!commitEdit && this._hadSwatchChange) {
|
|
this._textEditor.undo();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} 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.Utils.isWordChar(char) || char === '.' || char === '-' || char === '$';
|
|
}
|
|
|
|
/**
|
|
* @param {!TextUtils.TextRange.TextRange} prefixRange
|
|
* @param {!TextUtils.TextRange.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.cssMetadata().propertyValues(tokenContent);
|
|
return Promise.resolve(propertyValues.filter(value => value.startsWith(prefix)).map(value => {
|
|
return {
|
|
text: value,
|
|
title: undefined,
|
|
subtitle: undefined,
|
|
iconType: undefined,
|
|
priority: undefined,
|
|
isSecondary: undefined,
|
|
subtitleRenderer: undefined,
|
|
selectionRange: undefined,
|
|
hideGhostText: undefined,
|
|
iconElement: undefined,
|
|
};
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @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(), SwatchBookmark).forEach(marker => marker.clear());
|
|
this._textEditor.element.removeEventListener('keydown', /** @type {!EventListener} */ (this._boundHandleKeyDown));
|
|
}
|
|
}
|
|
|
|
/** @type {number} */
|
|
export const maxSwatchProcessingLength = 300;
|
|
|
|
/** @type {symbol} */
|
|
export const SwatchBookmark = Symbol('swatch');
|
|
|
|
/** @type {!WeakMap<!Element, !TextEditor.CodeMirrorTextEditor.TextEditorBookMark>} */
|
|
const swatchToBookmark = new WeakMap();
|