mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 21:28:43 +08:00
R=jacktfranklin@chromium.org No-Presubmit: True Bug: 1082789 Change-Id: Ic77884bf7e9b5dc74053915b3b10699368841b66 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2595268 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
386 lines
10 KiB
JavaScript
386 lines
10 KiB
JavaScript
// Copyright 2016 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 '../common/common.js';
|
|
import * as FormatterActions from '../formatter_worker/FormatterActions.js'; // eslint-disable-line rulesdir/es_modules_import
|
|
|
|
const MAX_WORKERS = Math.min(2, navigator.hardwareConcurrency - 1);
|
|
|
|
/** @type {!FormatterWorkerPool} */
|
|
let formatterWorkerPoolInstance;
|
|
|
|
export class FormatterWorkerPool {
|
|
constructor() {
|
|
/** @type {!Array<!Task>} */
|
|
this._taskQueue = [];
|
|
/** @type {!Map<!Common.Worker.WorkerWrapper, ?Task>} */
|
|
this._workerTasks = new Map();
|
|
}
|
|
|
|
/**
|
|
* @return {!FormatterWorkerPool}
|
|
*/
|
|
static instance() {
|
|
if (!formatterWorkerPoolInstance) {
|
|
formatterWorkerPoolInstance = new FormatterWorkerPool();
|
|
}
|
|
|
|
return formatterWorkerPoolInstance;
|
|
}
|
|
|
|
/**
|
|
* @return {!Common.Worker.WorkerWrapper}
|
|
*/
|
|
_createWorker() {
|
|
const worker = Common.Worker.WorkerWrapper.fromURL(
|
|
new URL('../formatter_worker/formatter_worker-entrypoint.js', import.meta.url));
|
|
worker.onmessage = this._onWorkerMessage.bind(this, worker);
|
|
worker.onerror = this._onWorkerError.bind(this, worker);
|
|
return worker;
|
|
}
|
|
|
|
_processNextTask() {
|
|
if (!this._taskQueue.length) {
|
|
return;
|
|
}
|
|
|
|
let freeWorker = [...this._workerTasks.keys()].find(worker => !this._workerTasks.get(worker));
|
|
if (!freeWorker && this._workerTasks.size < MAX_WORKERS) {
|
|
freeWorker = this._createWorker();
|
|
}
|
|
if (!freeWorker) {
|
|
return;
|
|
}
|
|
|
|
const task = this._taskQueue.shift();
|
|
if (task) {
|
|
this._workerTasks.set(freeWorker, task);
|
|
freeWorker.postMessage({method: task.method, params: task.params});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Worker.WorkerWrapper} worker
|
|
* @param {!MessageEvent} event
|
|
*/
|
|
_onWorkerMessage(worker, event) {
|
|
const task = this._workerTasks.get(worker);
|
|
if (!task) {
|
|
return;
|
|
}
|
|
if (task.isChunked && event.data && !event.data['isLastChunk']) {
|
|
task.callback(event.data);
|
|
return;
|
|
}
|
|
|
|
this._workerTasks.set(worker, null);
|
|
this._processNextTask();
|
|
task.callback(event.data ? event.data : null);
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Worker.WorkerWrapper} worker
|
|
* @param {!Event} event
|
|
*/
|
|
_onWorkerError(worker, event) {
|
|
console.error(event);
|
|
const task = this._workerTasks.get(worker);
|
|
worker.terminate();
|
|
this._workerTasks.delete(worker);
|
|
|
|
const newWorker = this._createWorker();
|
|
this._workerTasks.set(newWorker, null);
|
|
this._processNextTask();
|
|
if (task) {
|
|
task.callback(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} methodName
|
|
* @param {!Object<string, string>} params
|
|
* @param {function(boolean, *):void} callback
|
|
*/
|
|
_runChunkedTask(methodName, params, callback) {
|
|
const task = new Task(methodName, params, onData, true);
|
|
this._taskQueue.push(task);
|
|
this._processNextTask();
|
|
|
|
/**
|
|
* @param {?Object} data
|
|
*/
|
|
function onData(data) {
|
|
if (!data) {
|
|
callback(true, null);
|
|
return;
|
|
}
|
|
const isLastChunk = 'isLastChunk' in data && Boolean(data['isLastChunk']);
|
|
const chunk = 'chunk' in data && data['chunk'];
|
|
callback(isLastChunk, chunk);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!FormatterActions.FormatterActions} methodName
|
|
* @param {!Object<string, string>} params
|
|
* @return {!Promise<*>}
|
|
*/
|
|
_runTask(methodName, params) {
|
|
return new Promise(resolve => {
|
|
const task = new Task(methodName, params, resolve, false);
|
|
this._taskQueue.push(task);
|
|
this._processNextTask();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} mimeType
|
|
* @param {string} content
|
|
* @param {string} indentString
|
|
* @return {!Promise<!FormatResult>}
|
|
*/
|
|
format(mimeType, content, indentString) {
|
|
const parameters = {mimeType: mimeType, content: content, indentString: indentString};
|
|
return /** @type {!Promise<!FormatResult>} */ (this._runTask(FormatterActions.FormatterActions.FORMAT, parameters));
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @return {!Promise<!Array<!{name: string, offset: number}>>}
|
|
*/
|
|
javaScriptIdentifiers(content) {
|
|
return this._runTask(FormatterActions.FormatterActions.JAVASCRIPT_IDENTIFIERS, {content: content})
|
|
.then(ids => ids || []);
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @return {!Promise<string>}
|
|
*/
|
|
evaluatableJavaScriptSubstring(content) {
|
|
return this._runTask(FormatterActions.FormatterActions.EVALUATE_JAVASCRIPT_SUBSTRING, {content: content})
|
|
.then(text => text || '');
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @param {function(boolean, !Array<!CSSRule>):void} callback
|
|
*/
|
|
parseCSS(content, callback) {
|
|
this._runChunkedTask(FormatterActions.FormatterActions.PARSE_CSS, {content: content}, onDataChunk);
|
|
|
|
/**
|
|
* @param {boolean} isLastChunk
|
|
* @param {*} data
|
|
*/
|
|
function onDataChunk(isLastChunk, data) {
|
|
const rules = /** @type {!Array<!CSSRule>} */ (data || []);
|
|
callback(isLastChunk, rules);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @param {function(boolean, !Array<!JSOutlineItem>):void} callback
|
|
*/
|
|
javaScriptOutline(content, callback) {
|
|
this._runChunkedTask(FormatterActions.FormatterActions.JAVASCRIPT_OUTLINE, {content: content}, onDataChunk);
|
|
|
|
/**
|
|
* @param {boolean} isLastChunk
|
|
* @param {*} data
|
|
*/
|
|
function onDataChunk(isLastChunk, data) {
|
|
const items = /** @type {!Array.<!JSOutlineItem>} */ (data || []);
|
|
callback(isLastChunk, items);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @param {string} mimeType
|
|
* @param {function(boolean, !Array<!OutlineItem>):void} callback
|
|
* @return {boolean}
|
|
*/
|
|
outlineForMimetype(content, mimeType, callback) {
|
|
switch (mimeType) {
|
|
case 'text/html':
|
|
case 'text/javascript':
|
|
this.javaScriptOutline(content, javaScriptCallback);
|
|
return true;
|
|
case 'text/css':
|
|
this.parseCSS(content, cssCallback);
|
|
return true;
|
|
}
|
|
return false;
|
|
|
|
/**
|
|
* @param {boolean} isLastChunk
|
|
* @param {!Array<!JSOutlineItem>} items
|
|
*/
|
|
function javaScriptCallback(isLastChunk, items) {
|
|
callback(
|
|
isLastChunk,
|
|
items.map(item => ({line: item.line, column: item.column, title: item.name, subtitle: item.arguments})));
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} isLastChunk
|
|
* @param {!Array<!CSSRule>} rules
|
|
*/
|
|
function cssCallback(isLastChunk, rules) {
|
|
callback(isLastChunk, rules.map(rule => {
|
|
const title = 'selectorText' in rule ? rule.selectorText : rule.atRule;
|
|
return {line: rule.lineNumber, subtitle: undefined, column: rule.columnNumber, title};
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @return {!Promise<?string>}
|
|
*/
|
|
findLastExpression(content) {
|
|
return /** @type {!Promise<?string>} */ (
|
|
this._runTask(FormatterActions.FormatterActions.FIND_LAST_EXPRESSION, {content}));
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @return {!Promise<?{baseExpression: string, receiver: string, argumentIndex: number, functionName: string}>}
|
|
*/
|
|
findLastFunctionCall(content) {
|
|
return /** @type {!Promise<?{baseExpression: string, receiver: string, argumentIndex: number, functionName: string}>} */ (
|
|
this._runTask(FormatterActions.FormatterActions.FIND_LAST_FUNCTION_CALL, {content}));
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @return {!Promise<!Array<string>>}
|
|
*/
|
|
argumentsList(content) {
|
|
return /** @type {!Promise<!Array<string>>} */ (
|
|
this._runTask(FormatterActions.FormatterActions.ARGUMENTS_LIST, {content}));
|
|
}
|
|
}
|
|
|
|
class Task {
|
|
/**
|
|
* @param {string} method
|
|
* @param {!Object<string, string>} params
|
|
* @param {function(?MessageEvent):void} callback
|
|
* @param {boolean=} isChunked
|
|
*/
|
|
constructor(method, params, callback, isChunked) {
|
|
this.method = method;
|
|
this.params = params;
|
|
this.callback = callback;
|
|
this.isChunked = isChunked;
|
|
}
|
|
}
|
|
|
|
export class FormatResult {
|
|
constructor() {
|
|
/** @type {string} */
|
|
this.content;
|
|
/** @type {!FormatMapping} */
|
|
this.mapping;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
class JSOutlineItem {
|
|
constructor() {
|
|
/** @type {string} */
|
|
this.name;
|
|
/** @type {(string|undefined)} */
|
|
this.arguments;
|
|
/** @type {number} */
|
|
this.line;
|
|
/** @type {number} */
|
|
this.column;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
class CSSProperty {
|
|
constructor() {
|
|
/** @type {string} */
|
|
this.name;
|
|
/** @type {!TextRange} */
|
|
this.nameRange;
|
|
/** @type {string} */
|
|
this.value;
|
|
/** @type {!TextRange} */
|
|
this.valueRange;
|
|
/** @type {!TextRange} */
|
|
this.range;
|
|
/** @type {(boolean|undefined)} */
|
|
this.disabled;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
class SCSSProperty {
|
|
constructor() {
|
|
/** @type {!TextRange} */
|
|
this.range;
|
|
/** @type {!TextRange} */
|
|
this.name;
|
|
/** @type {!TextRange} */
|
|
this.value;
|
|
/** @type {boolean} */
|
|
this.disabled;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
class SCSSRule {
|
|
constructor() {
|
|
/** @type {!Array<!TextRange>} */
|
|
this.selectors;
|
|
/** @type {!Array<!SCSSProperty>} */
|
|
this.properties;
|
|
/** @type {!TextRange} */
|
|
this.styleRange;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!FormatterWorkerPool}
|
|
*/
|
|
export function formatterWorkerPool() {
|
|
return FormatterWorkerPool.instance();
|
|
}
|
|
|
|
/** @typedef {{line: number, column: number, title: string, subtitle: (string|undefined) }} */
|
|
// @ts-ignore typedef
|
|
export let OutlineItem;
|
|
|
|
/** @typedef {{original: !Array<number>, formatted: !Array<number>}} */
|
|
// @ts-ignore typedef
|
|
export let FormatMapping;
|
|
|
|
/** @typedef {{selectorText: string, styleRange: !TextRange, lineNumber: number, columnNumber: number, properties: !Array<!CSSProperty>}} */
|
|
// @ts-ignore typedef
|
|
export let CSSStyleRule;
|
|
|
|
/**
|
|
* @typedef {{atRule: string, lineNumber: number, columnNumber: number}}
|
|
*/
|
|
// @ts-ignore typedef
|
|
export let CSSAtRule;
|
|
|
|
/**
|
|
* @typedef {(CSSStyleRule|CSSAtRule)}
|
|
*/
|
|
// @ts-ignore typedef
|
|
export let CSSRule;
|
|
|
|
/**
|
|
* @typedef {{startLine: number, startColumn: number, endLine: number, endColumn: number}}
|
|
*/
|
|
// @ts-ignore typedef
|
|
export let TextRange;
|