tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

editor.js (12723B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import { showMenu } from "../../context-menu/menu";
      6 
      7 import { copyToTheClipboard } from "../../utils/clipboard";
      8 import {
      9  getRawSourceURL,
     10  shouldBlackbox,
     11  findBlackBoxRange,
     12 } from "../../utils/source";
     13 import { toSourceLine } from "../../utils/editor/index";
     14 import { downloadFile } from "../../utils/utils";
     15 import { features } from "../../utils/prefs";
     16 import { isFulfilled } from "../../utils/async-value";
     17 
     18 import { createBreakpointItems } from "./editor-breakpoint";
     19 
     20 import {
     21  getPrettySource,
     22  getIsCurrentThreadPaused,
     23  isSourceWithMap,
     24  getBlackBoxRanges,
     25  isSourceOnSourceMapIgnoreList,
     26  isSourceMapIgnoreListEnabled,
     27  getEditorWrapping,
     28 } from "../../selectors/index";
     29 
     30 import { continueToHere } from "../../actions/pause/continueToHere";
     31 import { jumpToMappedLocation } from "../../actions/sources/select";
     32 import {
     33  showSource,
     34  toggleInlinePreview,
     35  toggleEditorWrapping,
     36 } from "../../actions/ui";
     37 import { toggleBlackBox } from "../../actions/sources/blackbox";
     38 import { addExpression } from "../../actions/expressions";
     39 import { evaluateInConsole } from "../../actions/toolbox";
     40 
     41 export function showEditorContextMenu(event, editor, lineObject, location) {
     42  return async ({ dispatch, getState }) => {
     43    const { source } = location;
     44    const state = getState();
     45    const blackboxedRanges = getBlackBoxRanges(state);
     46    const isPaused = getIsCurrentThreadPaused(state);
     47    const hasMappedLocation =
     48      (source.isOriginal ||
     49        isSourceWithMap(state, source.id) ||
     50        source.isPrettyPrinted) &&
     51      !getPrettySource(state, source.id);
     52    const isSourceOnIgnoreList =
     53      isSourceMapIgnoreListEnabled(state) &&
     54      isSourceOnSourceMapIgnoreList(state, source);
     55    const editorWrappingEnabled = getEditorWrapping(state);
     56 
     57    showMenu(
     58      event,
     59      editorMenuItems({
     60        blackboxedRanges,
     61        hasMappedLocation,
     62        location,
     63        isPaused,
     64        editorWrappingEnabled,
     65        selectionText: editor.getSelectedText(),
     66        isTextSelected: editor.isTextSelected(),
     67        lineObject,
     68        isSourceOnIgnoreList,
     69        dispatch,
     70      })
     71    );
     72  };
     73 }
     74 
     75 export function showEditorGutterContextMenu(event, line, location, lineText) {
     76  return async ({ dispatch, getState }) => {
     77    const { source } = location;
     78    const state = getState();
     79    const blackboxedRanges = getBlackBoxRanges(state);
     80    const isPaused = getIsCurrentThreadPaused(state);
     81    const isSourceOnIgnoreList =
     82      isSourceMapIgnoreListEnabled(state) &&
     83      isSourceOnSourceMapIgnoreList(state, source);
     84 
     85    showMenu(event, [
     86      ...createBreakpointItems(location, lineText, dispatch),
     87      { type: "separator" },
     88      continueToHereItem(location, isPaused, dispatch),
     89      { type: "separator" },
     90      blackBoxLineMenuItem(
     91        source,
     92        line,
     93        blackboxedRanges,
     94        isSourceOnIgnoreList,
     95        location.line,
     96        dispatch
     97      ),
     98    ]);
     99  };
    100 }
    101 
    102 // Menu Items
    103 const continueToHereItem = (location, isPaused, dispatch) => ({
    104  accesskey: L10N.getStr("editor.continueToHere.accesskey"),
    105  disabled: !isPaused,
    106  click: () => dispatch(continueToHere(location)),
    107  id: "node-menu-continue-to-here",
    108  label: L10N.getStr("editor.continueToHere.label"),
    109 });
    110 
    111 const copyToClipboardItem = selectionText => ({
    112  id: "node-menu-copy-to-clipboard",
    113  label: L10N.getStr("copyToClipboard.label"),
    114  accesskey: L10N.getStr("copyToClipboard.accesskey"),
    115  disabled: selectionText.length === 0,
    116  click: () => copyToTheClipboard(selectionText),
    117 });
    118 
    119 const copySourceItem = selectedContent => ({
    120  id: "node-menu-copy-source",
    121  label: L10N.getStr("copySource.label"),
    122  accesskey: L10N.getStr("copySource.accesskey"),
    123  disabled: false,
    124  click: () =>
    125    selectedContent.type === "text" &&
    126    copyToTheClipboard(selectedContent.value),
    127 });
    128 
    129 const copySourceUri2Item = selectedSource => ({
    130  id: "node-menu-copy-source-url",
    131  label: L10N.getStr("copySourceUri2"),
    132  accesskey: L10N.getStr("copySourceUri2.accesskey"),
    133  disabled: !selectedSource.url,
    134  click: () => copyToTheClipboard(getRawSourceURL(selectedSource.url)),
    135 });
    136 
    137 const jumpToMappedLocationItem = (location, hasMappedLocation, dispatch) => ({
    138  id: "node-menu-jump",
    139  label: L10N.getFormatStr(
    140    "editor.jumpToMappedLocation1",
    141    location.source.isOriginal
    142      ? L10N.getStr("generated")
    143      : L10N.getStr("original")
    144  ),
    145  accesskey: L10N.getStr("editor.jumpToMappedLocation1.accesskey"),
    146  disabled: !hasMappedLocation,
    147  click: () => dispatch(jumpToMappedLocation(location)),
    148 });
    149 
    150 const showSourceMenuItem = (selectedSource, dispatch) => ({
    151  id: "node-menu-show-source",
    152  label: L10N.getStr("sourceTabs.revealInTree"),
    153  accesskey: L10N.getStr("sourceTabs.revealInTree.accesskey"),
    154  disabled: !selectedSource.url,
    155  click: () => dispatch(showSource(selectedSource.id)),
    156 });
    157 
    158 const blackBoxMenuItem = (
    159  selectedSource,
    160  blackboxedRanges,
    161  isSourceOnIgnoreList,
    162  dispatch
    163 ) => {
    164  const isBlackBoxed = !!blackboxedRanges[selectedSource.url];
    165  return {
    166    id: "node-menu-blackbox",
    167    label: isBlackBoxed
    168      ? L10N.getStr("ignoreContextItem.unignore")
    169      : L10N.getStr("ignoreContextItem.ignore"),
    170    accesskey: isBlackBoxed
    171      ? L10N.getStr("ignoreContextItem.unignore.accesskey")
    172      : L10N.getStr("ignoreContextItem.ignore.accesskey"),
    173    disabled: isSourceOnIgnoreList || !shouldBlackbox(selectedSource),
    174    click: () => dispatch(toggleBlackBox(selectedSource)),
    175  };
    176 };
    177 
    178 const blackBoxLineMenuItem = (
    179  selectedSource,
    180  { from, to },
    181  blackboxedRanges,
    182  isSourceOnIgnoreList,
    183  // the clickedLine is passed when the context menu
    184  // is opened from the gutter, it is not available when the
    185  // the context menu is opened from the editor.
    186  clickedLine = null,
    187  dispatch
    188 ) => {
    189  const startLine = clickedLine ?? toSourceLine(selectedSource, from.line);
    190  const endLine = clickedLine ?? toSourceLine(selectedSource, to.line);
    191 
    192  const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
    193    start: startLine,
    194    end: endLine,
    195  });
    196 
    197  const selectedLineIsBlackBoxed = !!blackboxRange;
    198 
    199  const isSingleLine = selectedLineIsBlackBoxed
    200    ? blackboxRange.start.line == blackboxRange.end.line
    201    : startLine == endLine;
    202 
    203  const isSourceFullyBlackboxed =
    204    blackboxedRanges[selectedSource.url] &&
    205    !blackboxedRanges[selectedSource.url].length;
    206 
    207  // The ignore/unignore line context menu item should be disabled when
    208  // 1) The source is on the sourcemap ignore list
    209  // 2) The whole source is blackboxed or
    210  // 3) Multiple lines are blackboxed or
    211  // 4) Multiple lines are selected in the editor
    212  const shouldDisable =
    213    isSourceOnIgnoreList || isSourceFullyBlackboxed || !isSingleLine;
    214 
    215  return {
    216    id: "node-menu-blackbox-line",
    217    label: !selectedLineIsBlackBoxed
    218      ? L10N.getStr("ignoreContextItem.ignoreLine")
    219      : L10N.getStr("ignoreContextItem.unignoreLine"),
    220    accesskey: !selectedLineIsBlackBoxed
    221      ? L10N.getStr("ignoreContextItem.ignoreLine.accesskey")
    222      : L10N.getStr("ignoreContextItem.unignoreLine.accesskey"),
    223    disabled: shouldDisable,
    224    click: () => {
    225      const selectionRange = {
    226        start: {
    227          line: startLine,
    228          column: clickedLine == null ? from.ch : 0,
    229        },
    230        end: {
    231          line: endLine,
    232          column: clickedLine == null ? to.ch : 0,
    233        },
    234      };
    235 
    236      dispatch(
    237        toggleBlackBox(
    238          selectedSource,
    239          !selectedLineIsBlackBoxed,
    240          selectedLineIsBlackBoxed ? [blackboxRange] : [selectionRange]
    241        )
    242      );
    243    },
    244  };
    245 };
    246 
    247 const blackBoxLinesMenuItem = (
    248  selectedSource,
    249  { from, to },
    250  blackboxedRanges,
    251  isSourceOnIgnoreList,
    252  clickedLine,
    253  dispatch
    254 ) => {
    255  const startLine = toSourceLine(selectedSource, from.line);
    256  const endLine = toSourceLine(selectedSource, to.line);
    257 
    258  const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
    259    start: startLine,
    260    end: endLine,
    261  });
    262 
    263  const selectedLinesAreBlackBoxed = !!blackboxRange;
    264 
    265  return {
    266    id: "node-menu-blackbox-lines",
    267    label: !selectedLinesAreBlackBoxed
    268      ? L10N.getStr("ignoreContextItem.ignoreLines")
    269      : L10N.getStr("ignoreContextItem.unignoreLines"),
    270    accesskey: !selectedLinesAreBlackBoxed
    271      ? L10N.getStr("ignoreContextItem.ignoreLines.accesskey")
    272      : L10N.getStr("ignoreContextItem.unignoreLines.accesskey"),
    273    disabled: isSourceOnIgnoreList,
    274    click: () => {
    275      const selectionRange = {
    276        start: {
    277          line: startLine,
    278          column: from.ch,
    279        },
    280        end: {
    281          line: endLine,
    282          column: to.ch,
    283        },
    284      };
    285 
    286      dispatch(
    287        toggleBlackBox(
    288          selectedSource,
    289          !selectedLinesAreBlackBoxed,
    290          selectedLinesAreBlackBoxed ? [blackboxRange] : [selectionRange]
    291        )
    292      );
    293    },
    294  };
    295 };
    296 
    297 const watchExpressionItem = (selectedSource, selectionText, dispatch) => ({
    298  id: "node-menu-add-watch-expression",
    299  label: L10N.getStr("expressions.label"),
    300  accesskey: L10N.getStr("expressions.accesskey"),
    301  click: () => dispatch(addExpression(selectionText)),
    302 });
    303 
    304 const evaluateInConsoleItem = (selectedSource, selectionText, dispatch) => ({
    305  id: "node-menu-evaluate-in-console",
    306  label: L10N.getStr("evaluateInConsole.label"),
    307  click: () => dispatch(evaluateInConsole(selectionText)),
    308 });
    309 
    310 const downloadFileItem = (selectedSource, selectedContent) => ({
    311  id: "node-menu-download-file",
    312  label: L10N.getStr("downloadFile.label"),
    313  accesskey: L10N.getStr("downloadFile.accesskey"),
    314  click: () => downloadFile(selectedContent, selectedSource.shortName),
    315 });
    316 
    317 const inlinePreviewItem = dispatch => ({
    318  id: "node-menu-inline-preview",
    319  label: features.inlinePreview
    320    ? L10N.getStr("inlinePreview.hide.label")
    321    : L10N.getStr("inlinePreview.show.label"),
    322  click: () => dispatch(toggleInlinePreview(!features.inlinePreview)),
    323 });
    324 
    325 const editorWrappingItem = (editorWrappingEnabled, dispatch) => ({
    326  id: "node-menu-editor-wrapping",
    327  label: editorWrappingEnabled
    328    ? L10N.getStr("editorWrapping.hide.label")
    329    : L10N.getStr("editorWrapping.show.label"),
    330  click: () => dispatch(toggleEditorWrapping(!editorWrappingEnabled)),
    331 });
    332 
    333 function editorMenuItems({
    334  blackboxedRanges,
    335  location,
    336  selectionText,
    337  hasMappedLocation,
    338  isTextSelected,
    339  isPaused,
    340  editorWrappingEnabled,
    341  lineObject,
    342  isSourceOnIgnoreList,
    343  dispatch,
    344 }) {
    345  const items = [];
    346 
    347  const { source } = location;
    348 
    349  const content =
    350    source.content && isFulfilled(source.content) ? source.content.value : null;
    351 
    352  items.push(
    353    jumpToMappedLocationItem(location, hasMappedLocation, dispatch),
    354    continueToHereItem(location, isPaused, dispatch),
    355    { type: "separator" },
    356    copyToClipboardItem(selectionText),
    357    ...(!source.isWasm
    358      ? [
    359          ...(content ? [copySourceItem(content)] : []),
    360          copySourceUri2Item(source),
    361        ]
    362      : []),
    363    ...(content ? [downloadFileItem(source, content)] : []),
    364    { type: "separator" },
    365    showSourceMenuItem(source, dispatch),
    366    { type: "separator" },
    367    blackBoxMenuItem(source, blackboxedRanges, isSourceOnIgnoreList, dispatch)
    368  );
    369 
    370  const startLine = toSourceLine(source, lineObject.from.line);
    371  const endLine = toSourceLine(source, lineObject.to.line);
    372 
    373  // Find any blackbox ranges that exist for the selected lines
    374  const blackboxRange = findBlackBoxRange(source, blackboxedRanges, {
    375    start: startLine,
    376    end: endLine,
    377  });
    378 
    379  const isMultiLineSelection = blackboxRange
    380    ? blackboxRange.start.line !== blackboxRange.end.line
    381    : startLine !== endLine;
    382 
    383  // When the range is defined and is an empty array,
    384  // the whole source is blackboxed
    385  const theWholeSourceIsBlackBoxed =
    386    blackboxedRanges[source.url] && !blackboxedRanges[source.url].length;
    387 
    388  if (!theWholeSourceIsBlackBoxed) {
    389    const blackBoxSourceLinesMenuItem = isMultiLineSelection
    390      ? blackBoxLinesMenuItem
    391      : blackBoxLineMenuItem;
    392 
    393    items.push(
    394      blackBoxSourceLinesMenuItem(
    395        source,
    396        lineObject,
    397        blackboxedRanges,
    398        isSourceOnIgnoreList,
    399        null,
    400        dispatch
    401      )
    402    );
    403  }
    404 
    405  if (isTextSelected) {
    406    items.push(
    407      { type: "separator" },
    408      watchExpressionItem(source, selectionText, dispatch),
    409      evaluateInConsoleItem(source, selectionText, dispatch)
    410    );
    411  }
    412 
    413  items.push(
    414    { type: "separator" },
    415    inlinePreviewItem(dispatch),
    416    editorWrappingItem(editorWrappingEnabled, dispatch)
    417  );
    418 
    419  return items;
    420 }