UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

388 lines 15.3 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as ColorPicker from '../../ui/legacy/components/color_picker/color_picker.js'; import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js'; import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import * as UI from '../../ui/legacy/legacy.js'; import { assertNotNullOrUndefined } from '../../core/platform/platform.js'; import { Plugin } from './Plugin.js'; // Plugin to add CSS completion, shortcuts, and color/curve swatches // to editors with CSS content. 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('panels/sources/CSSPlugin.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const dontCompleteIn = new Set(['ColorLiteral', 'NumberLiteral', 'StringLiteral', 'Comment', 'Important']); function findPropertyAt(node, pos) { if (dontCompleteIn.has(node.name)) { return null; } for (let cur = node; cur; cur = cur.parent) { if (cur.name === 'StyleSheet' || cur.name === 'Styles' || cur.name === 'CallExpression') { break; } else if (cur.name === 'Declaration') { const name = cur.getChild('PropertyName'), colon = cur.getChild(':'); return name && colon && colon.to <= pos ? name : null; } } return null; } function getCurrentStyleSheet(url, cssModel) { const currentStyleSheet = cssModel.getStyleSheetIdsForURL(url); if (currentStyleSheet.length === 0) { Platform.DCHECK(() => currentStyleSheet.length !== 0, 'Can\'t find style sheet ID for current URL'); } return currentStyleSheet[0]; } async function specificCssCompletion(cx, uiSourceCode, cssModel) { const node = CodeMirror.syntaxTree(cx.state).resolveInner(cx.pos, -1); if (node.name === 'ClassName') { // Should never happen, but let's code defensively here assertNotNullOrUndefined(cssModel); const currentStyleSheet = getCurrentStyleSheet(uiSourceCode.url(), cssModel); const existingClassNames = await cssModel.getClassNames(currentStyleSheet); return { from: node.from, options: existingClassNames.map(value => ({ type: 'constant', label: value })), }; } const property = findPropertyAt(node, cx.pos); if (property) { const propertyValues = SDK.CSSMetadata.cssMetadata().getPropertyValues(cx.state.sliceDoc(property.from, property.to)); return { from: node.name === 'ValueName' ? node.from : cx.pos, options: propertyValues.map(value => ({ type: 'constant', label: value })), validFor: /^[\w\P{ASCII}\-]+$/u, }; } return null; } function findColorsAndCurves(state, from, to, onColor, onCurve) { let line = state.doc.lineAt(from); function getToken(from, to) { if (from >= line.to) { line = state.doc.lineAt(from); } return line.text.slice(from - line.from, to - line.from); } const tree = CodeMirror.ensureSyntaxTree(state, to, 100); if (!tree) { return; } tree.iterate({ from, to, enter: node => { let content; if (node.name === 'ValueName' || node.name === 'ColorLiteral') { content = getToken(node.from, node.to); } else if (node.name === 'Callee' && /^(?:(?:rgba?|hsla?|hwba?|lch|oklch|lab|oklab|color)|cubic-bezier)$/.test(getToken(node.from, node.to))) { content = state.sliceDoc(node.from, node.node.parent.to); } if (content) { const parsedColor = Common.Color.parse(content); if (parsedColor) { onColor(node.from, parsedColor, content); } else { const parsedCurve = UI.Geometry.CubicBezier.parse(content); if (parsedCurve) { onCurve(node.from, parsedCurve, content); } } } }, }); } class ColorSwatchWidget extends CodeMirror.WidgetType { #text; #color; #from; constructor(color, text, from) { super(); this.#color = color; this.#text = text; this.#from = from; } eq(other) { return this.#color.equal(other.#color) && this.#text === other.#text && this.#from === other.#from; } toDOM(view) { const swatch = new InlineEditor.ColorSwatch.ColorSwatch(); swatch.renderColor(this.#color, false, i18nString(UIStrings.openColorPicker)); const value = swatch.createChild('span'); value.textContent = this.#text; value.setAttribute('hidden', 'true'); swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, event => { view.dispatch({ changes: { from: this.#from, to: this.#from + this.#text.length, insert: event.data.text }, }); this.#text = event.data.text; this.#color = swatch.getColor(); }); swatch.addEventListener(InlineEditor.ColorSwatch.ClickEvent.eventName, event => { event.consume(true); view.dispatch({ effects: setTooltip.of({ type: 0 /* TooltipType.Color */, pos: view.posAtDOM(swatch), text: this.#text, swatch, color: this.#color, }), }); }); return swatch; } ignoreEvent() { return true; } } class CurveSwatchWidget extends CodeMirror.WidgetType { curve; text; constructor(curve, text) { super(); this.curve = curve; this.text = text; } eq(other) { return this.curve.asCSSText() === other.curve.asCSSText() && this.text === other.text; } toDOM(view) { const swatch = InlineEditor.Swatches.BezierSwatch.create(); swatch.setBezierText(this.text); UI.Tooltip.Tooltip.install(swatch.iconElement(), i18nString(UIStrings.openCubicBezierEditor)); swatch.iconElement().addEventListener('click', (event) => { event.consume(true); view.dispatch({ effects: setTooltip.of({ type: 1 /* TooltipType.Curve */, pos: view.posAtDOM(swatch), text: this.text, swatch, curve: this.curve, }), }); }, false); swatch.hideText(true); return swatch; } ignoreEvent() { return true; } } function createCSSTooltip(active) { return { pos: active.pos, arrow: true, create(view) { let text = active.text; let widget, addListener; if (active.type === 0 /* TooltipType.Color */) { const spectrum = new ColorPicker.Spectrum.Spectrum(); addListener = (handler) => { spectrum.addEventListener(ColorPicker.Spectrum.Events.ColorChanged, handler); }; spectrum.addEventListener(ColorPicker.Spectrum.Events.SizeChanged, () => view.requestMeasure()); spectrum.setColor(active.color, active.color.format()); widget = spectrum; Host.userMetrics.colorPickerOpenedFrom(0 /* Host.UserMetrics.ColorPickerOpenedFrom.SourcesPanel */); } else { const spectrum = new InlineEditor.BezierEditor.BezierEditor(active.curve); widget = spectrum; addListener = (handler) => { spectrum.addEventListener(InlineEditor.BezierEditor.Events.BezierChanged, handler); }; } const dom = document.createElement('div'); dom.className = 'cm-tooltip-swatchEdit'; widget.markAsRoot(); widget.show(dom); widget.showWidget(); widget.element.addEventListener('keydown', event => { if (event.key === 'Escape') { event.consume(); view.dispatch({ effects: setTooltip.of(null), changes: text === active.text ? undefined : { from: active.pos, to: active.pos + text.length, insert: active.text }, }); widget.hideWidget(); view.focus(); } }); widget.element.addEventListener('focusout', event => { if (event.relatedTarget && !widget.element.contains(event.relatedTarget)) { view.dispatch({ effects: setTooltip.of(null) }); widget.hideWidget(); } }, false); widget.element.addEventListener('mousedown', event => event.consume()); return { dom, resize: false, offset: { x: -8, y: 0 }, mount: () => { widget.focus(); widget.wasShown(); addListener((event) => { view.dispatch({ changes: { from: active.pos, to: active.pos + text.length, insert: event.data }, annotations: isSwatchEdit.of(true), }); text = event.data; }); }, }; }, }; } const setTooltip = CodeMirror.StateEffect.define(); const isSwatchEdit = CodeMirror.Annotation.define(); const cssTooltipState = CodeMirror.StateField.define({ create() { return null; }, update(value, tr) { if ((tr.docChanged || tr.selection) && !tr.annotation(isSwatchEdit)) { value = null; } for (const effect of tr.effects) { if (effect.is(setTooltip)) { value = effect.value; } } return value; }, provide: field => CodeMirror.showTooltip.from(field, active => active && createCSSTooltip(active)), }); function computeSwatchDeco(state, from, to) { const builder = new CodeMirror.RangeSetBuilder(); findColorsAndCurves(state, from, to, (pos, parsedColor, colorText) => { builder.add(pos, pos, CodeMirror.Decoration.widget({ widget: new ColorSwatchWidget(parsedColor, colorText, pos) })); }, (pos, curve, text) => { builder.add(pos, pos, CodeMirror.Decoration.widget({ widget: new CurveSwatchWidget(curve, text) })); }); return builder.finish(); } const cssSwatchPlugin = CodeMirror.ViewPlugin.fromClass(class { decorations; constructor(view) { this.decorations = computeSwatchDeco(view.state, view.viewport.from, view.viewport.to); } update(update) { if (update.viewportChanged || update.docChanged) { this.decorations = computeSwatchDeco(update.state, update.view.viewport.from, update.view.viewport.to); } } }, { decorations: v => v.decorations, }); function cssSwatches() { return [cssSwatchPlugin, cssTooltipState]; } function getNumberAt(node) { if (node.name === 'Unit') { node = node.parent; } if (node.name === 'NumberLiteral') { const lastChild = node.lastChild; return { from: node.from, to: lastChild && lastChild.name === 'Unit' ? lastChild.from : node.to }; } return null; } function modifyUnit(view, by) { const { head } = view.state.selection.main; const context = CodeMirror.syntaxTree(view.state).resolveInner(head, -1); const numberRange = getNumberAt(context) || getNumberAt(context.resolve(head, 1)); if (!numberRange) { return false; } const currentNumber = Number(view.state.sliceDoc(numberRange.from, numberRange.to)); if (isNaN(currentNumber)) { return false; } view.dispatch({ changes: { from: numberRange.from, to: numberRange.to, insert: String(currentNumber + by) }, scrollIntoView: true, userEvent: 'insert.modifyUnit', }); return true; } export function cssBindings() { // This is an awkward way to pass the argument given to the editor // event handler through the ShortcutRegistry calling convention. let currentView = null; const listener = UI.ShortcutRegistry.ShortcutRegistry.instance().getShortcutListener({ 'sources.increment-css': () => Promise.resolve(modifyUnit(currentView, 1)), 'sources.increment-css-by-ten': () => Promise.resolve(modifyUnit(currentView, 10)), 'sources.decrement-css': () => Promise.resolve(modifyUnit(currentView, -1)), 'sources.decrement-css-by-ten': () => Promise.resolve(modifyUnit(currentView, -10)), }); return CodeMirror.EditorView.domEventHandlers({ keydown: (event, view) => { const prevView = currentView; currentView = view; listener(event); currentView = prevView; return event.defaultPrevented; }, }); } export class CSSPlugin extends Plugin { #cssModel; constructor(uiSourceCode, _transformer) { super(uiSourceCode, _transformer); SDK.TargetManager.TargetManager.instance().observeModels(SDK.CSSModel.CSSModel, this); } static accepts(uiSourceCode) { return uiSourceCode.contentType().hasStyleSheets(); } modelAdded(cssModel) { if (cssModel.target() !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) { return; } this.#cssModel = cssModel; } modelRemoved(cssModel) { if (this.#cssModel === cssModel) { this.#cssModel = undefined; } } editorExtension() { return [cssBindings(), this.#cssCompletion(), cssSwatches()]; } #cssCompletion() { const { cssCompletionSource } = CodeMirror.css; // CodeMirror binds the function below to the state object. // Therefore, we can't access `this` and retrieve the following properties. // Instead, retrieve them up front to bind them to the correct closure. const uiSourceCode = this.uiSourceCode; const cssModel = this.#cssModel; return CodeMirror.autocompletion({ override: [async (cx) => { return (await specificCssCompletion(cx, uiSourceCode, cssModel)) || cssCompletionSource(cx); }], }); } } //# sourceMappingURL=CSSPlugin.js.map