Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/devtools/client/webconsole/components/Input/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 49 kB image not shown  

Quelle  JSTerm.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const { debounce } = require("resource://devtools/shared/debounce.js");
const isMacOS = Services.appinfo.OS === "Darwin";

loader.lazyRequireGetter(this"Debugger""Debugger");
loader.lazyRequireGetter(
  this,
  "EventEmitter",
  "resource://devtools/shared/event-emitter.js"
);
loader.lazyRequireGetter(
  this,
  "AutocompletePopup",
  "resource://devtools/client/shared/autocomplete-popup.js"
);

loader.lazyRequireGetter(
  this,
  "PropTypes",
  "resource://devtools/client/shared/vendor/react-prop-types.js"
);
loader.lazyRequireGetter(
  this,
  "KeyCodes",
  "resource://devtools/client/shared/keycodes.js",
  true
);
loader.lazyRequireGetter(
  this,
  "Editor",
  "resource://devtools/client/shared/sourceeditor/editor.js"
);
loader.lazyRequireGetter(
  this,
  "getFocusableElements",
  "resource://devtools/client/shared/focus.js",
  true
);
loader.lazyRequireGetter(
  this,
  "l10n",
  "resource://devtools/client/webconsole/utils/messages.js",
  true
);
loader.lazyRequireGetter(
  this,
  "saveAs",
  "resource://devtools/shared/DevToolsUtils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "beautify",
  "resource://devtools/shared/jsbeautify/beautify.js"
);

// React & Redux
const {
  Component,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const {
  connect,
} = require("resource://devtools/client/shared/vendor/react-redux.js");

// History Modules
const {
  getHistory,
  getHistoryValue,
} = require("resource://devtools/client/webconsole/selectors/history.js");
const {
  getAutocompleteState,
} = require("resource://devtools/client/webconsole/selectors/autocomplete.js");
const actions = require("resource://devtools/client/webconsole/actions/index.js");

const EvaluationContextSelector = createFactory(
  require("resource://devtools/client/webconsole/components/Input/EvaluationContextSelector.js")
);

// Constants used for defining the direction of JSTerm input history navigation.
const {
  HISTORY_BACK,
  HISTORY_FORWARD,
} = require("resource://devtools/client/webconsole/constants.js");

const JSTERM_CODEMIRROR_ORIGIN = "jsterm";

/**
 * Create a JSTerminal (a JavaScript command line). This is attached to an
 * existing HeadsUpDisplay (a Web Console instance). This code is responsible
 * with handling command line input and code evaluation.
 */

class JSTerm extends Component {
  static get propTypes() {
    return {
      // Returns previous or next value from the history
      // (depending on direction argument).
      getValueFromHistory: PropTypes.func.isRequired,
      // History of executed expression (state).
      history: PropTypes.object.isRequired,
      // Console object.
      webConsoleUI: PropTypes.object.isRequired,
      // Needed for opening context menu
      serviceContainer: PropTypes.object.isRequired,
      // Handler for clipboard 'paste' event (also used for 'drop' event, callback).
      onPaste: PropTypes.func,
      // Evaluate provided expression.
      evaluateExpression: PropTypes.func.isRequired,
      // Update position in the history after executing an expression (action).
      updateHistoryPosition: PropTypes.func.isRequired,
      // Update autocomplete popup state.
      autocompleteUpdate: PropTypes.func.isRequired,
      autocompleteClear: PropTypes.func.isRequired,
      // Data to be displayed in the autocomplete popup.
      autocompleteData: PropTypes.object.isRequired,
      // Toggle the editor mode.
      editorToggle: PropTypes.func.isRequired,
      // Dismiss the editor onboarding UI.
      editorOnboardingDismiss: PropTypes.func.isRequired,
      // Set the last JS input value.
      terminalInputChanged: PropTypes.func.isRequired,
      // Is the input in editor mode.
      editorMode: PropTypes.bool,
      editorWidth: PropTypes.number,
      editorPrettifiedAt: PropTypes.number,
      showEditorOnboarding: PropTypes.bool,
      autocomplete: PropTypes.bool,
      showEvaluationContextSelector: PropTypes.bool,
      autocompletePopupPosition: PropTypes.string,
      inputEnabled: PropTypes.bool,
    };
  }

  constructor(props) {
    super(props);

    const { webConsoleUI } = props;

    this.webConsoleUI = webConsoleUI;
    this.hudId = this.webConsoleUI.hudId;

    this._onEditorChanges = this._onEditorChanges.bind(this);
    this._onEditorBeforeChange = this._onEditorBeforeChange.bind(this);
    this._onEditorKeyHandled = this._onEditorKeyHandled.bind(this);
    this.onContextMenu = this.onContextMenu.bind(this);
    this.imperativeUpdate = this.imperativeUpdate.bind(this);

    // We debounce the autocompleteUpdate so we don't send too many requests to the server
    // as the user is typing.
    // The delay should be small enough to be unnoticed by the user.
    this.autocompleteUpdate = debounce(this.props.autocompleteUpdate, 75, this);

    // Updates to the terminal input which can trigger eager evaluations are
    // similarly debounced.
    this.terminalInputChanged = debounce(
      this.props.terminalInputChanged,
      75,
      this
    );

    // Because the autocomplete has a slight delay (75ms), there can be time where the
    // codeMirror completion text is out-of-date, which might lead to issue when the user
    // accept the autocompletion while the update of the completion text is still pending.
    // In order to account for that, we put any future value of the completion text in
    // this property.
    this.pendingCompletionText = null;

    /**
     * Last input value.
     * @type string
     */

    this.lastInputValue = "";

    this.autocompletePopup = null;

    EventEmitter.decorate(this);
    webConsoleUI.jsterm = this;
  }

  componentDidMount() {
    if (this.props.editorMode) {
      this.setEditorWidth(this.props.editorWidth);
    }

    const autocompleteOptions = {
      onSelect: this.onAutocompleteSelect.bind(this),
      onClick: this.acceptProposedCompletion.bind(this),
      listId: "webConsole_autocompletePopupListBox",
      position: this.props.autocompletePopupPosition,
      autoSelect: true,
      useXulWrapper: true,
    };

    const doc = this.webConsoleUI.document;
    const { toolbox } = this.webConsoleUI.wrapper;
    const tooltipDoc = toolbox ? toolbox.doc : doc;
    // The popup will be attached to the toolbox document or HUD document in the case
    // such as the browser console which doesn't have a toolbox.
    this.autocompletePopup = new AutocompletePopup(
      tooltipDoc,
      autocompleteOptions
    );

    if (this.node) {
      const onArrowUp = () => {
        let inputUpdated;
        if (this.autocompletePopup.isOpen) {
          this.autocompletePopup.selectPreviousItem();
          return null;
        }

        if (this.props.editorMode === false && this.canCaretGoPrevious()) {
          inputUpdated = this.historyPeruse(HISTORY_BACK);
        }

        return inputUpdated ? null : "CodeMirror.Pass";
      };

      const onArrowDown = () => {
        let inputUpdated;
        if (this.autocompletePopup.isOpen) {
          this.autocompletePopup.selectNextItem();
          return null;
        }

        if (this.props.editorMode === false && this.canCaretGoNext()) {
          inputUpdated = this.historyPeruse(HISTORY_FORWARD);
        }

        return inputUpdated ? null : "CodeMirror.Pass";
      };

      const onArrowLeft = () => {
        if (this.autocompletePopup.isOpen || this.getAutoCompletionText()) {
          this.clearCompletion();
        }
        return "CodeMirror.Pass";
      };

      const onArrowRight = () => {
        // We only want to complete on Right arrow if the completion text is
        // displayed.
        if (this.getAutoCompletionText()) {
          this.acceptProposedCompletion();
          return null;
        }

        this.clearCompletion();
        return "CodeMirror.Pass";
      };

      const onCtrlCmdEnter = () => {
        if (this.hasAutocompletionSuggestion()) {
          return this.acceptProposedCompletion();
        }

        this._execute();
        return null;
      };

      this.editor = new Editor({
        autofocus: true,
        enableCodeFolding: this.props.editorMode,
        lineNumbers: this.props.editorMode,
        lineWrapping: true,
        mode: {
          name: "javascript",
          globalVars: true,
        },
        styleActiveLine: false,
        tabIndex: "0",
        viewportMargin: Infinity,
        disableSearchAddon: true,
        extraKeys: {
          Enter: () => {
            // No need to handle shift + Enter as it's natively handled by CodeMirror.

            const hasSuggestion = this.hasAutocompletionSuggestion();
            if (
              !hasSuggestion &&
              !Debugger.isCompilableUnit(this._getValue())
            ) {
              // incomplete statement
              return "CodeMirror.Pass";
            }

            if (hasSuggestion) {
              return this.acceptProposedCompletion();
            }

            if (!this.props.editorMode) {
              this._execute();
              return null;
            }
            return "CodeMirror.Pass";
          },

          "Cmd-Enter": onCtrlCmdEnter,
          "Ctrl-Enter": onCtrlCmdEnter,

          [Editor.accel("S")]: () => {
            const value = this._getValue();
            if (!value) {
              return null;
            }

            const date = new Date();
            const suggestedName =
              `console-input-${date.getFullYear()}-` +
              `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
              `${date.getMinutes()}-${date.getSeconds()}.js`;
            const data = new TextEncoder().encode(value);
            return saveAs(window, data, suggestedName, [
              {
                pattern: "*.js",
                label: l10n.getStr("webconsole.input.openJavaScriptFileFilter"),
              },
            ]);
          },

          [Editor.accel("O")]: async () => this._openFile(),

          Tab: () => {
            if (this.hasEmptyInput()) {
              this.editor.codeMirror.getInputField().blur();
              return false;
            }

            if (
              this.props.autocompleteData &&
              this.props.autocompleteData.getterPath
            ) {
              this.props.autocompleteUpdate(
                true,
                this.props.autocompleteData.getterPath
              );
              return false;
            }

            const isSomethingSelected = this.editor.somethingSelected();
            const hasSuggestion = this.hasAutocompletionSuggestion();

            if (hasSuggestion && !isSomethingSelected) {
              this.acceptProposedCompletion();
              return false;
            }

            if (!isSomethingSelected) {
              this.insertStringAtCursor("\t");
              return false;
            }

            // Something is selected, let the editor handle the indent.
            return true;
          },

          "Shift-Tab": () => {
            if (this.hasEmptyInput()) {
              this.focusPreviousElement();
              return false;
            }

            const hasSuggestion = this.hasAutocompletionSuggestion();

            if (hasSuggestion) {
              return false;
            }

            return "CodeMirror.Pass";
          },

          Up: onArrowUp,
          "Cmd-Up": onArrowUp,

          Down: onArrowDown,
          "Cmd-Down": onArrowDown,

          Left: onArrowLeft,
          "Ctrl-Left": onArrowLeft,
          "Cmd-Left": onArrowLeft,
          "Alt-Left": onArrowLeft,
          // On OSX, Ctrl-A navigates to the beginning of the line.
          "Ctrl-A": isMacOS ? onArrowLeft : undefined,

          Right: onArrowRight,
          "Ctrl-Right": onArrowRight,
          "Cmd-Right": onArrowRight,
          "Alt-Right": onArrowRight,

          "Ctrl-N": () => {
            // Control-N differs from down arrow: it ignores autocomplete state.
            // Note that we preserve the default 'down' navigation within
            // multiline text.
            if (
              Services.appinfo.OS === "Darwin" &&
              this.props.editorMode === false &&
              this.canCaretGoNext() &&
              this.historyPeruse(HISTORY_FORWARD)
            ) {
              return null;
            }

            this.clearCompletion();
            return "CodeMirror.Pass";
          },

          "Ctrl-P": () => {
            // Control-P differs from up arrow: it ignores autocomplete state.
            // Note that we preserve the default 'up' navigation within
            // multiline text.
            if (
              Services.appinfo.OS === "Darwin" &&
              this.props.editorMode === false &&
              this.canCaretGoPrevious() &&
              this.historyPeruse(HISTORY_BACK)
            ) {
              return null;
            }

            this.clearCompletion();
            return "CodeMirror.Pass";
          },

          PageUp: () => {
            if (this.autocompletePopup.isOpen) {
              this.autocompletePopup.selectPreviousPageItem();
            } else {
              const { outputScroller } = this.webConsoleUI;
              const { scrollTop, clientHeight } = outputScroller;
              outputScroller.scrollTop = Math.max(0, scrollTop - clientHeight);
            }

            return null;
          },

          PageDown: () => {
            if (this.autocompletePopup.isOpen) {
              this.autocompletePopup.selectNextPageItem();
            } else {
              const { outputScroller } = this.webConsoleUI;
              const { scrollTop, scrollHeight, clientHeight } = outputScroller;
              outputScroller.scrollTop = Math.min(
                scrollHeight,
                scrollTop + clientHeight
              );
            }

            return null;
          },

          Home: () => {
            if (this.autocompletePopup.isOpen) {
              this.autocompletePopup.selectItemAtIndex(0);
              return null;
            }

            if (!this._getValue()) {
              this.webConsoleUI.outputScroller.scrollTop = 0;
              return null;
            }

            if (this.getAutoCompletionText()) {
              this.clearCompletion();
            }

            return "CodeMirror.Pass";
          },

          End: () => {
            if (this.autocompletePopup.isOpen) {
              this.autocompletePopup.selectItemAtIndex(
                this.autocompletePopup.itemCount - 1
              );
              return null;
            }

            if (!this._getValue()) {
              const { outputScroller } = this.webConsoleUI;
              outputScroller.scrollTop = outputScroller.scrollHeight;
              return null;
            }

            if (this.getAutoCompletionText()) {
              this.clearCompletion();
            }

            return "CodeMirror.Pass";
          },

          "Ctrl-Space": () => {
            if (!this.autocompletePopup.isOpen) {
              this.props.autocompleteUpdate(
                true,
                null,
                this._getExpressionVariables()
              );
              return null;
            }

            return "CodeMirror.Pass";
          },

          Esc: false,
          // Don't handle Ctrl/Cmd + F so it can be listened by a parent node
          [Editor.accel("F")]: false,
        },
      });

      this.editor.on("changes"this._onEditorChanges);
      this.editor.on("beforeChange"this._onEditorBeforeChange);
      this.editor.on("blur"this._onEditorBlur);
      this.editor.on("keyHandled"this._onEditorKeyHandled);

      this.editor.appendToLocalElement(this.node);
      const cm = this.editor.codeMirror;
      cm.on("paste", (_, event) => this.props.onPaste(event));
      cm.on("drop", (_, event) => this.props.onPaste(event));

      this.node.addEventListener("keydown", event => {
        if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
          if (this.autocompletePopup.isOpen) {
            this.clearCompletion();
            event.preventDefault();
            event.stopPropagation();
          }

          if (
            this.props.autocompleteData &&
            this.props.autocompleteData.getterPath
          ) {
            this.props.autocompleteClear();
            event.preventDefault();
            event.stopPropagation();
          }
        }
      });

      this.resizeObserver = new ResizeObserver(() => {
        // If we don't have the node reference, or if the node isn't connected
        // anymore, we disconnect the resize observer (componentWillUnmount is never
        // called on this component, so we have to do it here).
        if (!this.node || !this.node.isConnected) {
          this.resizeObserver.disconnect();
          return;
        }
        // Calling `refresh` will update the cursor position, and all the selection blocks.
        this.editor.codeMirror.refresh();
      });
      this.resizeObserver.observe(this.node);

      // Update the character width needed for the popup offset calculations.
      this._inputCharWidth = this._getInputCharWidth();
      this.lastInputValue && this._setValue(this.lastInputValue);
    }
  }

  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
  UNSAFE_componentWillReceiveProps(nextProps) {
    this.imperativeUpdate(nextProps);
  }

  shouldComponentUpdate(nextProps) {
    return (
      this.props.showEditorOnboarding !== nextProps.showEditorOnboarding ||
      this.props.editorMode !== nextProps.editorMode
    );
  }

  /**
   * Do all the imperative work needed after a Redux store update.
   *
   * @param {Object} nextProps: props passed from shouldComponentUpdate.
   */

  imperativeUpdate(nextProps) {
    if (!nextProps) {
      return;
    }

    if (
      nextProps.autocompleteData !== this.props.autocompleteData &&
      nextProps.autocompleteData.pendingRequestId === null
    ) {
      this.updateAutocompletionPopup(nextProps.autocompleteData);
    }

    if (nextProps.editorMode !== this.props.editorMode) {
      if (this.editor) {
        this.editor.setOption("lineNumbers", nextProps.editorMode);
        this.editor.setOption("enableCodeFolding", nextProps.editorMode);
      }

      if (nextProps.editorMode && nextProps.editorWidth) {
        this.setEditorWidth(nextProps.editorWidth);
      } else {
        this.setEditorWidth(null);
      }

      if (this.autocompletePopup.isOpen) {
        this.autocompletePopup.hidePopup();
      }
    }

    if (
      nextProps.autocompletePopupPosition !==
        this.props.autocompletePopupPosition &&
      this.autocompletePopup
    ) {
      this.autocompletePopup.position = nextProps.autocompletePopupPosition;
    }

    if (
      nextProps.editorPrettifiedAt &&
      nextProps.editorPrettifiedAt !== this.props.editorPrettifiedAt
    ) {
      this._setValue(
        beautify.js(this._getValue(), {
          // Read directly from prefs because this.editor.config.indentUnit and
          // this.editor.getOption('indentUnit') are not really synced with
          // prefs.
          indent_size: Services.prefs.getIntPref("devtools.editor.tabsize"),
          indent_with_tabs: !Services.prefs.getBoolPref(
            "devtools.editor.expandtab"
          ),
        })
      );
    }
  }

  /**
   *
   * @param {Number|null} editorWidth: The width to set the node to. If null, removes any
   *                                   `width` property on node style.
   */

  setEditorWidth(editorWidth) {
    if (!this.node) {
      return;
    }

    if (editorWidth) {
      this.node.style.width = `${editorWidth}px`;
    } else {
      this.node.style.removeProperty("width");
    }
  }

  focus() {
    if (this.editor) {
      this.editor.focus();
    }
  }

  focusPreviousElement() {
    const inputField = this.editor.codeMirror.getInputField();

    const findPreviousFocusableElement = el => {
      if (!el || !el.querySelectorAll) {
        return null;
      }

      // We only want to get visible focusable element, and for that we can assert that
      // the offsetParent isn't null. We can do that because we don't have fixed position
      // element in the console.
      const items = getFocusableElements(el).filter(
        ({ offsetParent }) => offsetParent !== null
      );
      const inputIndex = items.indexOf(inputField);

      if (items.length === 0 || (inputIndex > -1 && items.length === 1)) {
        return findPreviousFocusableElement(el.parentNode);
      }

      const index = inputIndex > 0 ? inputIndex - 1 : items.length - 1;
      return items[index];
    };

    const focusableEl = findPreviousFocusableElement(this.node.parentNode);
    if (focusableEl) {
      focusableEl.focus();
    }
  }

  /**
   * Execute a string. Execution happens asynchronously in the content process.
   */

  _execute() {
    const value = this._getValue();
    // In editor mode, we only evaluate the text selection if there's one. The feature isn't
    // enabled in inline mode as it can be confusing since input is cleared when evaluating.
    const executeString = this.props.editorMode
      ? this.getSelectedText() || value
      : value;

    if (!executeString) {
      return;
    }

    if (!this.props.editorMode) {
      // Calling this.props.terminalInputChanged instead of this.terminalInputChanged
      // because we want to instantly hide the instant evaluation result, and don't want
      // the delay we have in this.terminalInputChanged.
      this.props.terminalInputChanged("");
      this._setValue("");
    }
    this.clearCompletion();
    this.props.evaluateExpression(executeString);
  }

  /**
   * Sets the value of the input field.
   *
   * @param string newValue
   *        The new value to set.
   * @returns void
   */

  _setValue(newValue = "") {
    this.lastInputValue = newValue;
    this.terminalInputChanged(newValue);

    if (this.editor) {
      // In order to get the autocomplete popup to work properly, we need to set the
      // editor text and the cursor in the same operation. If we don't, the text change
      // is done before the cursor is moved, and the autocompletion call to the server
      // sends an erroneous query.
      this.editor.codeMirror.operation(() => {
        this.editor.setText(newValue);

        // Set the cursor at the end of the input.
        const lines = newValue.split("\n");
        this.editor.setCursor({
          line: lines.length - 1,
          ch: lines[lines.length - 1].length,
        });
        this.editor.setAutoCompletionText();
      });
    }

    this.emitForTests("set-input-value");
  }

  /**
   * Gets the value from the input field
   * @returns string
   */

  _getValue() {
    return this.editor ? this.editor.getText() || "" : "";
  }

  /**
   * Open the file picker for the user to select a javascript file and open it.
   *
   */

  async _openFile() {
    const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    fp.init(
      this.webConsoleUI.document.defaultView.browsingContext,
      l10n.getStr("webconsole.input.openJavaScriptFile"),
      Ci.nsIFilePicker.modeOpen
    );

    // Append file filters
    fp.appendFilter(
      l10n.getStr("webconsole.input.openJavaScriptFileFilter"),
      "*.js"
    );

    function readFile(file) {
      return new Promise(resolve => {
        IOUtils.read(file.path).then(data => {
          const decoder = new TextDecoder();
          resolve(decoder.decode(data));
        });
      });
    }

    const content = await new Promise(resolve => {
      fp.open(rv => {
        if (rv == Ci.nsIFilePicker.returnOK) {
          const file = Cc["@mozilla.org/file/local;1"].createInstance(
            Ci.nsIFile
          );
          file.initWithPath(fp.file.path);
          readFile(file).then(resolve);
        }
      });
    });

    this._setValue(content);
  }

  getSelectionStart() {
    return this.getInputValueBeforeCursor().length;
  }

  getSelectedText() {
    return this.editor.getSelection();
  }

  /**
   * Even handler for the "beforeChange" event fired by codeMirror. This event is fired
   * when codeMirror is about to make a change to its DOM representation.
   */

  _onEditorBeforeChange(cm, change) {
    // If the user did not type a character that matches the completion text, then we
    // clear it before the change is done to prevent a visual glitch.
    // See Bugs 1491776 & 1558248.
    const { from, to, origin, text } = change;
    const isAddedText =
      from.line === to.line && from.ch === to.ch && origin === "+input";

    // if there was no changes (hitting delete on an empty input, or suppr when at the end
    // of the input), we bail out.
    if (
      !isAddedText &&
      origin === "+delete" &&
      from.line === to.line &&
      from.ch === to.ch
    ) {
      return;
    }

    const addedText = text.join("");
    const completionText = this.getAutoCompletionText();

    const addedCharacterMatchCompletion =
      isAddedText && completionText.startsWith(addedText);

    const addedCharacterMatchPopupItem =
      isAddedText &&
      this.autocompletePopup.items.some(({ preLabel, label }) =>
        label.startsWith(preLabel + addedText)
      );
    const nextSelectedAutocompleteItemIndex =
      addedCharacterMatchPopupItem &&
      this.autocompletePopup.items.findIndex(({ preLabel, label }) =>
        label.startsWith(preLabel + addedText)
      );

    if (addedCharacterMatchPopupItem) {
      this.autocompletePopup.selectItemAtIndex(
        nextSelectedAutocompleteItemIndex,
        { preventSelectCallback: true }
      );
    }

    if (!completionText || change.canceled || !addedCharacterMatchCompletion) {
      this.setAutoCompletionText("");
    }

    if (!addedCharacterMatchCompletion && !addedCharacterMatchPopupItem) {
      this.autocompletePopup.hidePopup();
    } else if (
      !change.canceled &&
      (completionText ||
        addedCharacterMatchCompletion ||
        addedCharacterMatchPopupItem)
    ) {
      // The completion text will be updated when the debounced autocomplete update action
      // is done, so in the meantime we set the pending value to pendingCompletionText.
      // See Bug 1595068 for more information.
      this.pendingCompletionText = completionText.substring(text.length);
      // And we update the preLabel of the matching autocomplete items that may be used
      // in the acceptProposedAutocompletion function.
      this.autocompletePopup.items.forEach(item => {
        if (item.label.startsWith(item.preLabel + addedText)) {
          item.preLabel += addedText;
        }
      });
    }
  }

  /**
   * Even handler for the "blur" event fired by codeMirror.
   */

  _onEditorBlur(cm) {
    if (cm.somethingSelected()) {
      // If there's a selection when the input is blurred, then we remove it by setting
      // the cursor at the position that matches the start of the first selection.
      const [{ head }] = cm.listSelections();
      cm.setCursor(head, { scroll: false });
    }
  }

  /**
   * Fired after a key is handled through a key map.
   *
   * @param {CodeMirror} cm: codeMirror instance
   * @param {String} key: The key that was handled
   */

  _onEditorKeyHandled(cm, key) {
    // The autocloseBracket addon handle closing brackets keys when they're typed, but
    // there's already an existing closing bracket.
    // ex:
    //  1. input is `foo(x|)` (where | represents the cursor)
    //  2. user types `)`
    //  3. input is now `foo(x)|` (i.e. the typed character wasn't inserted)
    // In such case, _onEditorBeforeChange isn't triggered, so we need to hide the popup
    // here. We can do that because this function won't be called when codeMirror _do_
    // insert the closing char.
    const closingKeys = [`']'`, `')'`, "'}'"];
    if (this.autocompletePopup.isOpen && closingKeys.includes(key)) {
      this.clearCompletion();
    }
  }

  /**
   * Retrieve variable declared in the expression from the CodeMirror state, in order
   * to display them in the autocomplete popup.
   */

  _getExpressionVariables() {
    const cm = this.editor.codeMirror;
    const { state } = cm.getTokenAt(cm.getCursor());
    const variables = [];

    if (state.context) {
      for (let c = state.context; c; c = c.prev) {
        for (let v = c.vars; v; v = v.next) {
          if (v.name) {
            variables.push(v.name);
          }
        }
      }
    }

    const keys = ["localVars""globalVars"];
    for (const key of keys) {
      if (state[key]) {
        for (let v = state[key]; v; v = v.next) {
          if (v.name) {
            variables.push(v.name);
          }
        }
      }
    }

    return variables;
  }

  /**
   * The editor "changes" event handler.
   */

  _onEditorChanges(cm, changes) {
    const value = this._getValue();

    if (this.lastInputValue !== value) {
      // We don't autocomplete if the changes were made by JsTerm (e.g. autocomplete was
      // accepted).
      const isJsTermChangeOnly = changes.every(
        ({ origin }) => origin === JSTERM_CODEMIRROR_ORIGIN
      );

      if (
        !isJsTermChangeOnly &&
        (this.props.autocomplete || this.hasAutocompletionSuggestion())
      ) {
        this.autocompleteUpdate(falsenullthis._getExpressionVariables());
      }
      this.lastInputValue = value;
      this.terminalInputChanged(value);
    }
  }

  /**
   * Go up/down the history stack of input values.
   *
   * @param number direction
   *        History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
   *
   * @returns boolean
   *          True if the input value changed, false otherwise.
   */

  historyPeruse(direction) {
    const { history, updateHistoryPosition, getValueFromHistory } = this.props;

    if (!history.entries.length) {
      return false;
    }

    const newInputValue = getValueFromHistory(direction);
    const expression = this._getValue();
    updateHistoryPosition(direction, expression);

    if (newInputValue != null) {
      this._setValue(newInputValue);
      return true;
    }

    return false;
  }

  /**
   * Test for empty input.
   *
   * @return boolean
   */

  hasEmptyInput() {
    return this._getValue() === "";
  }

  /**
   * Check if the caret is at a location that allows selecting the previous item
   * in history when the user presses the Up arrow key.
   *
   * @return boolean
   *         True if the caret is at a location that allows selecting the
   *         previous item in history when the user presses the Up arrow key,
   *         otherwise false.
   */

  canCaretGoPrevious() {
    if (!this.editor) {
      return false;
    }

    const inputValue = this._getValue();
    const { line, ch } = this.editor.getCursor();
    return (line === 0 && ch === 0) || (line === 0 && ch === inputValue.length);
  }

  /**
   * Check if the caret is at a location that allows selecting the next item in
   * history when the user presses the Down arrow key.
   *
   * @return boolean
   *         True if the caret is at a location that allows selecting the next
   *         item in history when the user presses the Down arrow key, otherwise
   *         false.
   */

  canCaretGoNext() {
    if (!this.editor) {
      return false;
    }

    const inputValue = this._getValue();
    const multiline = /[\r\n]/.test(inputValue);

    const { line, ch } = this.editor.getCursor();
    return (
      (!multiline && ch === 0) ||
      this.editor.getDoc().getRange({ line: 0, ch: 0 }, { line, ch }).length ===
        inputValue.length
    );
  }

  /**
   * Takes the data returned by the server and update the autocomplete popup state (i.e.
   * its visibility and items).
   *
   * @param {Object} data
   *        The autocompletion data as returned by the webconsole actor's autocomplete
   *        service. Should be of the following shape:
   *        {
   *          matches: {Array} array of the properties matching the input,
   *          matchProp: {String} The string used to filter the properties,
   *          isElementAccess: {Boolean} True when the input is an element access,
   *                           i.e. `document["addEve`.
   *        }
   * @fires autocomplete-updated
   */

  async updateAutocompletionPopup(data) {
    if (!this.editor) {
      return;
    }

    const { matches, matchProp, isElementAccess } = data;
    if (!matches.length) {
      this.clearCompletion();
      return;
    }

    const inputUntilCursor = this.getInputValueBeforeCursor();

    const items = matches.map(label => {
      let preLabel = label.substring(0, matchProp.length);
      // If the user is performing an element access, and if they did not typed a quote,
      // then we need to adjust the preLabel to match the quote from the label + what
      // the user entered.
      if (isElementAccess && /^['"`]/.test(matchProp) === false) {
        preLabel = label.substring(0, matchProp.length + 1);
      }
      return { preLabel, label, isElementAccess };
    });

    if (items.length) {
      const { preLabel, label } = items[0];
      let suffix = label.substring(preLabel.length);
      if (isElementAccess) {
        if (!matchProp) {
          suffix = label;
        }
        const inputAfterCursor = this._getValue().substring(
          inputUntilCursor.length
        );
        // If there's not a bracket after the cursor, add it to the completionText.
        if (!inputAfterCursor.trimLeft().startsWith("]")) {
          suffix = suffix + "]";
        }
      }
      this.setAutoCompletionText(suffix);
    }

    const popup = this.autocompletePopup;
    // We don't want to trigger the onSelect callback since we already set the completion
    // text a few lines above.
    popup.setItems(items, 0, {
      preventSelectCallback: true,
    });

    const minimumAutoCompleteLength = 2;

    // We want to show the autocomplete popup if:
    // - there are at least 2 matching results
    // - OR, if there's 1 result, but whose label does not start like the input (this can
    //   happen with insensitive search: `num` will match `Number`).
    // - OR, if there's 1 result, but we can't show the completionText (because there's
    // some text after the cursor), unless the text in the popup is the same as the input.
    if (
      items.length >= minimumAutoCompleteLength ||
      (items.length === 1 && items[0].preLabel !== matchProp) ||
      (items.length === 1 &&
        !this.canDisplayAutoCompletionText() &&
        items[0].label !== matchProp)
    ) {
      // We need to show the popup at the "." or "[".
      const xOffset = -1 * matchProp.length * this._inputCharWidth;
      const yOffset = 5;
      const popupAlignElement =
        this.props.serviceContainer.getJsTermTooltipAnchor();
      this._openPopupPendingPromise = popup.openPopup(
        popupAlignElement,
        xOffset,
        yOffset,
        0,
        {
          preventSelectCallback: true,
        }
      );
      await this._openPopupPendingPromise;
      this._openPopupPendingPromise = null;
    } else if (
      items.length < minimumAutoCompleteLength &&
      (popup.isOpen || this._openPopupPendingPromise)
    ) {
      if (this._openPopupPendingPromise) {
        await this._openPopupPendingPromise;
      }
      popup.hidePopup();
    }

    // Eager evaluation results incorporate the current autocomplete item. We need to
    // trigger it here as well as in onAutocompleteSelect as we set the items with
    // preventSelectCallback (which means we won't trigger onAutocompleteSelect when the
    // popup is open).
    this.terminalInputChanged(
      this.getInputValueWithCompletionText().expression
    );

    this.emit("autocomplete-updated");
  }

  onAutocompleteSelect() {
    const { selectedItem } = this.autocompletePopup;
    if (selectedItem) {
      const { preLabel, label, isElementAccess } = selectedItem;
      let suffix = label.substring(preLabel.length);

      // If the user is performing an element access, we need to check if we should add
      // starting and ending quotes, as well as a closing bracket.
      if (isElementAccess) {
        const inputBeforeCursor = this.getInputValueBeforeCursor();
        if (inputBeforeCursor.trim().endsWith("[")) {
          suffix = label;
        }

        const inputAfterCursor = this._getValue().substring(
          inputBeforeCursor.length
        );
        // If there's no closing bracket after the cursor, add it to the completionText.
        if (!inputAfterCursor.trimLeft().startsWith("]")) {
          suffix = suffix + "]";
        }
      }
      this.setAutoCompletionText(suffix);
    } else {
      this.setAutoCompletionText("");
    }
    // Eager evaluation results incorporate the current autocomplete item.
    this.terminalInputChanged(
      this.getInputValueWithCompletionText().expression
    );
  }

  /**
   * Clear the current completion information, cancel any pending autocompletion update
   * and close the autocomplete popup, if needed.
   * @fires autocomplete-updated
   */

  clearCompletion() {
    this.autocompleteUpdate.cancel();
    // Update Eager evaluation result as the completion text was removed.
    this.terminalInputChanged(this._getValue());

    this.setAutoCompletionText("");
    let onPopupClosed = Promise.resolve();
    if (this.autocompletePopup) {
      this.autocompletePopup.clearItems();

      if (this.autocompletePopup.isOpen || this._openPopupPendingPromise) {
        onPopupClosed = this.autocompletePopup.once("popup-closed");

        if (this._openPopupPendingPromise) {
          this._openPopupPendingPromise.then(() =>
            this.autocompletePopup.hidePopup()
          );
        } else {
          this.autocompletePopup.hidePopup();
        }
        onPopupClosed.then(() => this.focus());
      }
    }
    onPopupClosed.then(() => this.emit("autocomplete-updated"));
  }

  /**
   * Accept the proposed input completion.
   */

  acceptProposedCompletion() {
    const {
      completionText,
      numberOfCharsToMoveTheCursorForward,
      numberOfCharsToReplaceCharsBeforeCursor,
    } = this.getInputValueWithCompletionText();

    this.autocompleteUpdate.cancel();
    this.props.autocompleteClear();

    // If the code triggering the opening of the popup was already triggered but not yet
    // settled, then we need to wait until it's resolved in order to close the popup (See
    // Bug 1655406).
    if (this._openPopupPendingPromise) {
      this._openPopupPendingPromise.then(() =>
        this.autocompletePopup.hidePopup()
      );
    }

    if (completionText) {
      this.insertStringAtCursor(
        completionText,
        numberOfCharsToReplaceCharsBeforeCursor
      );

      if (numberOfCharsToMoveTheCursorForward) {
        const { line, ch } = this.editor.getCursor();
        this.editor.setCursor({
          line,
          ch: ch + numberOfCharsToMoveTheCursorForward,
        });
      }
    }
  }

  /**
   * Returns an object containing the expression we would get if the user accepted the
   * current completion text. This is more than the current input + the completion text,
   * as there are special cases for element access and case-insensitive matches.
   *
   * @return {Object}: An object of the following shape:
   *         - {String} expression: The complete expression
   *         - {String} completionText: the completion text only, which should be used
   *                    with the next property
   *         - {Integer} numberOfCharsToReplaceCharsBeforeCursor: The number of chars that
   *                     should be removed from the current input before the cursor to
   *                     cleanly apply the completionText. This is handy when we only want
   *                     to insert the completionText.
   *         - {Integer} numberOfCharsToMoveTheCursorForward: The number of chars that the
   *                     cursor should be moved after the completion is done. This can
   *                     be useful for element access where there's already a closing
   *                     quote and/or bracket.
   */

  getInputValueWithCompletionText() {
    const inputBeforeCursor = this.getInputValueBeforeCursor();
    const inputAfterCursor = this._getValue().substring(
      inputBeforeCursor.length
    );
    let completionText = this.getAutoCompletionText();
    let numberOfCharsToReplaceCharsBeforeCursor;
    let numberOfCharsToMoveTheCursorForward = 0;

    // If the autocompletion popup is open, we always get the selected element from there,
    // since the autocompletion text might not be enough (e.g. `dOcUmEn` should
    // autocomplete to `document`, but the autocompletion text only shows `t`).
    if (this.autocompletePopup.isOpen && this.autocompletePopup.selectedItem) {
      const { selectedItem } = this.autocompletePopup;
      const { label, preLabel, isElementAccess } = selectedItem;

      completionText = label;
      numberOfCharsToReplaceCharsBeforeCursor = preLabel.length;

      // If the user is performing an element access, we need to check if we should add
      // starting and ending quotes, as well as a closing bracket.
      if (isElementAccess) {
        const lastOpeningBracketIndex = inputBeforeCursor.lastIndexOf("[");
        if (lastOpeningBracketIndex > -1) {
          numberOfCharsToReplaceCharsBeforeCursor = inputBeforeCursor.substring(
            lastOpeningBracketIndex + 1
          ).length;
        }

        // If the autoclose bracket option is enabled, the input might be in a state where
        // there's already the closing quote and the closing bracket, e.g.
        // `document["activeEl|"]`, so we don't need to add
        // Let's retrieve the completionText last character, to see if it's a quote.
        const completionTextLastChar =
          completionText[completionText.length - 1];
        const endingQuote = [`"`, `'`, "`"].includes(completionTextLastChar)
          ? completionTextLastChar
          : "";
        if (
          endingQuote &&
          inputAfterCursor.trimLeft().startsWith(endingQuote)
        ) {
          completionText = completionText.substring(
            0,
            completionText.length - 1
          );
          numberOfCharsToMoveTheCursorForward++;
        }

        // If there's not a closing bracket already, we add one.
        if (
          !inputAfterCursor.trimLeft().match(new RegExp(`^${endingQuote}?]`))
        ) {
          completionText = completionText + "]";
        } else {
          // if there's already one, we want to move the cursor after the closing bracket.
          numberOfCharsToMoveTheCursorForward++;
        }
      }
    }

    const expression =
      inputBeforeCursor.substring(
        0,
        inputBeforeCursor.length -
          (numberOfCharsToReplaceCharsBeforeCursor || 0)
      ) +
      completionText +
      inputAfterCursor;

    return {
      completionText,
      expression,
      numberOfCharsToMoveTheCursorForward,
      numberOfCharsToReplaceCharsBeforeCursor,
    };
  }

  getInputValueBeforeCursor() {
    return this.editor
      ? this.editor
          .getDoc()
          .getRange({ line: 0, ch: 0 }, this.editor.getCursor())
      : null;
  }

  /**
   * Insert a string into the console at the cursor location,
   * moving the cursor to the end of the string.
   *
   * @param {string} str
   * @param {int} numberOfCharsToReplaceCharsBeforeCursor - defaults to 0
   */

  insertStringAtCursor(str, numberOfCharsToReplaceCharsBeforeCursor = 0) {
    if (!this.editor) {
      return;
    }

    const cursor = this.editor.getCursor();
    const from = {
      line: cursor.line,
      ch: cursor.ch - numberOfCharsToReplaceCharsBeforeCursor,
    };

    this.editor
      .getDoc()
      .replaceRange(str, from, cursor, JSTERM_CODEMIRROR_ORIGIN);
  }

  /**
   * Set the autocompletion text of the input.
   *
   * @param string suffix
   *        The proposed suffix for the input value.
   */

  setAutoCompletionText(suffix) {
    if (!this.editor) {
      return;
    }

    this.pendingCompletionText = null;

    if (suffix && !this.canDisplayAutoCompletionText()) {
      suffix = "";
    }

    this.editor.setAutoCompletionText(suffix);
  }

  getAutoCompletionText() {
    const renderedCompletionText =
      this.editor && this.editor.getAutoCompletionText();
    return typeof this.pendingCompletionText === "string"
      ? this.pendingCompletionText
      : renderedCompletionText;
  }

  /**
   * Indicate if the input has an autocompletion suggestion, i.e. that there is either
   * something in the autocompletion text or that there's a selected item in the
   * autocomplete popup.
   */

  hasAutocompletionSuggestion() {
    // We can have cases where the popup is opened but we can't display the autocompletion
    // text.
    return (
      this.getAutoCompletionText() ||
      (this.autocompletePopup.isOpen &&
        Number.isInteger(this.autocompletePopup.selectedIndex) &&
        this.autocompletePopup.selectedIndex > -1)
    );
  }

  /**
   * Returns a boolean indicating if we can display an autocompletion text in the input,
   * i.e. if there is no characters displayed on the same line of the cursor and after it.
   */

  canDisplayAutoCompletionText() {
    if (!this.editor) {
      return false;
    }

    const { ch, line } = this.editor.getCursor();
    const lineContent = this.editor.getLine(line);
    const textAfterCursor = lineContent.substring(ch);
    return textAfterCursor === "";
  }

  /**
   * Calculates and returns the width of a single character of the input box.
   * This will be used in opening the popup at the correct offset.
   *
   * @returns {Number|null}: Width off the "x" char, or null if the input does not exist.
   */

  _getInputCharWidth() {
    return this.editor ? this.editor.defaultCharWidth() : null;
  }

  onContextMenu(e) {
    this.props.serviceContainer.openEditContextMenu(e);
  }

  destroy() {
    this.autocompleteUpdate.cancel();
    this.terminalInputChanged.cancel();
    this._openPopupPendingPromise = null;

    if (this.autocompletePopup) {
      this.autocompletePopup.destroy();
      this.autocompletePopup = null;
    }

    if (this.editor) {
      this.resizeObserver.disconnect();
      this.editor.destroy();
      this.editor = null;
    }

    this.webConsoleUI = null;
  }

  renderOpenEditorButton() {
    if (this.props.editorMode) {
      return null;
    }

    return dom.button({
      className:
        "devtools-button webconsole-input-openEditorButton" +
        (this.props.showEditorOnboarding ? " devtools-feature-callout" : ""),
      title: l10n.getFormatStr("webconsole.input.openEditorButton.tooltip2", [
        isMacOS ? "Cmd + B" : "Ctrl + B",
      ]),
      onClick: this.props.editorToggle,
    });
  }

  renderEvaluationContextSelector() {
    if (this.props.editorMode || !this.props.showEvaluationContextSelector) {
      return null;
    }

    return EvaluationContextSelector(this.props);
  }

  renderEditorOnboarding() {
    if (!this.props.showEditorOnboarding) {
      return null;
    }

    // We deliberately use getStr, and not getFormatStr, because we want keyboard
    // shortcuts to be wrapped in their own span.
    const label = l10n.getStr("webconsole.input.editor.onboarding.label");
    let [prefix, suffix] = label.split("%1$S");
    suffix = suffix.split("%2$S");

    const enterString = l10n.getStr("webconsole.enterKey");

    return dom.header(
      { className: "editor-onboarding" },
      dom.img({
        className: "editor-onboarding-fox",
        src: "chrome://devtools/skin/images/fox-smiling.svg",
      }),
      dom.p(
        {},
        prefix,
        dom.span({ className: "editor-onboarding-shortcut" }, enterString),
        suffix[0],
        dom.span({ className: "editor-onboarding-shortcut" }, [
          isMacOS ? `Cmd+${enterString}` : `Ctrl+${enterString}`,
        ]),
        suffix[1]
      ),
      dom.button(
        {
          className: "editor-onboarding-dismiss-button",
          onClick: () => this.props.editorOnboardingDismiss(),
        },
        l10n.getStr("webconsole.input.editor.onboarding.dismiss.label")
      )
    );
  }

  render() {
    if (!this.props.inputEnabled) {
      return null;
    }

    return dom.div(
      {
        className: "jsterm-input-container devtools-input",
        key: "jsterm-container",
        "aria-live""off",
        tabIndex: -1,
        onContextMenu: this.onContextMenu,
        ref: node => {
          this.node = node;
        },
      },
      dom.div(
        { className: "webconsole-input-buttons" },
        this.renderEvaluationContextSelector(),
        this.renderOpenEditorButton()
      ),
      this.renderEditorOnboarding()
    );
  }
}

// Redux connect

function mapStateToProps(state) {
  return {
    history: getHistory(state),
    getValueFromHistory: direction => getHistoryValue(state, direction),
    autocompleteData: getAutocompleteState(state),
    showEditorOnboarding: state.ui.showEditorOnboarding,
    showEvaluationContextSelector: state.ui.showEvaluationContextSelector,
    autocompletePopupPosition: state.prefs.eagerEvaluation ? "top" : "bottom",
    editorPrettifiedAt: state.ui.editorPrettifiedAt,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    updateHistoryPosition: (direction, expression) =>
      dispatch(actions.updateHistoryPosition(direction, expression)),
    autocompleteUpdate: (force, getterPath, expressionVars) =>
      dispatch(actions.autocompleteUpdate(force, getterPath, expressionVars)),
    autocompleteClear: () => dispatch(actions.autocompleteClear()),
    evaluateExpression: expression =>
      dispatch(actions.evaluateExpression(expression)),
    editorToggle: () => dispatch(actions.editorToggle()),
    editorOnboardingDismiss: () => dispatch(actions.editorOnboardingDismiss()),
    terminalInputChanged: value =>
      dispatch(actions.terminalInputChanged(value)),
  };
}

module.exports = connect(mapStateToProps, mapDispatchToProps)(JSTerm);

97%


¤ Dauer der Verarbeitung: 0.8 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung ist noch experimentell.