From 52dfa354784908428951063f20e735e81111c3db Mon Sep 17 00:00:00 2001 From: BinaryAge Bot Date: Fri, 30 Oct 2020 17:25:10 +0100 Subject: [PATCH] devtools -> dirac as of 9b75efc4a7135cc33a02fe450a0e1ee9f24d953a --- BUILD.gn | 1 + DEPS | 90 +- all_devtools_files.gni | 15 + all_devtools_modules.gni | 8 + devtools.iml | 10 + devtools_grd_files.gni | 12 + front_end/accessibility/ARIAAttributesView.js | 1 + front_end/bindings/ResourceScriptMapping.js | 7 + front_end/cm/cm.js | 4 + front_end/cm/module.json | 8 +- front_end/common/Settings.js | 2 +- front_end/components/Linkifier.js | 11 + front_end/components/components-legacy.js | 3 + front_end/console/BUILD.gn | 1 + front_end/console/ConsoleDiracPrompt.js | 953 +++++++++++ front_end/console/ConsoleView.js | 805 +++++++++- front_end/console/ConsoleViewMessage.js | 9 + front_end/console/Images | 1 + front_end/console/clojure-parinfer.js | 315 ++++ front_end/console/console.js | 3 + front_end/console/dirac-codemirror.css | 521 ++++++ front_end/console/dirac-hacks.css | 23 + front_end/console/dirac-prompt.css | 131 ++ front_end/console/dirac-theme.css | 88 + front_end/console/module.json | 19 +- front_end/dirac/dirac.js | 333 ++++ front_end/dirac/keysim.js | 803 ++++++++++ front_end/dirac/module.json | 21 + front_end/dirac/parinfer-codemirror.js | 685 ++++++++ front_end/dirac/parinfer.js | 1410 +++++++++++++++++ front_end/dirac/require-implant.js | 32 + front_end/dirac_lazy/dirac_lazy.js | 974 ++++++++++++ front_end/dirac_lazy/module.json | 16 + front_end/externs.js | 300 +++- front_end/host/InspectorFrontendHost.js | 24 +- front_end/inspector.js | 4 + front_end/main/MainImpl.js | 11 + front_end/main/module.json | 1 + .../object_ui/ObjectPropertiesSection.js | 105 +- .../object_ui/customPreviewComponent.css | 5 + .../object_ui/objectPropertiesSection.css | 24 +- front_end/protocol_client/module.json | 1 + front_end/screencast/ScreencastApp.js | 2 +- front_end/sdk/Connections.js | 2 +- front_end/sdk/ConsoleModel.js | 13 + front_end/sdk/DebuggerModel.js | 2 +- front_end/sdk/RuntimeModel.js | 34 + front_end/sdk/SourceMap.js | 24 + front_end/sdk/module.json | 83 +- front_end/shell.json | 2 + front_end/source_frame/SourcesTextEditor.js | 29 + front_end/sources/CallStackSidebarPane.js | 14 +- front_end/sources/DebuggerPlugin.js | 80 +- front_end/sources/SourceMapNamesResolver.js | 201 ++- front_end/text_editor/cmdevtools.css | 20 +- front_end/third_party/codemirror/BUILD.gn | 6 + .../package/addon/display/placeholder.d.ts | 1 + .../package/addon/runmode/runmode.d.ts | 1 + front_end/ui/InspectorView.js | 2 + front_end/ui/SuggestBox.js | 18 +- front_end/ui/TextPrompt.js | 29 +- front_end/ui/UIUtils.js | 3 + front_end/ui/inspectorViewTabbedPane.css | 4 + front_end/ui/suggestBox.css | 99 ++ front_end/ui/treeoutline.css | 23 + front_end/workspace/UISourceCode.js | 13 + scripts/build/pdl.py | 178 +++ scripts/check_gn.js | 5 +- scripts/closure/closure.iml | 18 + scripts/jsdoc_validator/jsdoc_validator.iml | 18 + scripts/migration/remove-unused-globals.sh | 12 +- 71 files changed, 8546 insertions(+), 180 deletions(-) create mode 100644 devtools.iml create mode 100644 front_end/console/ConsoleDiracPrompt.js create mode 120000 front_end/console/Images create mode 100644 front_end/console/clojure-parinfer.js create mode 100644 front_end/console/dirac-codemirror.css create mode 100644 front_end/console/dirac-hacks.css create mode 100644 front_end/console/dirac-prompt.css create mode 100644 front_end/console/dirac-theme.css create mode 100644 front_end/dirac/dirac.js create mode 100644 front_end/dirac/keysim.js create mode 100644 front_end/dirac/module.json create mode 100644 front_end/dirac/parinfer-codemirror.js create mode 100644 front_end/dirac/parinfer.js create mode 100644 front_end/dirac/require-implant.js create mode 100644 front_end/dirac_lazy/dirac_lazy.js create mode 100644 front_end/dirac_lazy/module.json create mode 100644 front_end/third_party/codemirror/package/addon/display/placeholder.d.ts create mode 100644 front_end/third_party/codemirror/package/addon/runmode/runmode.d.ts create mode 100644 scripts/build/pdl.py create mode 100644 scripts/closure/closure.iml create mode 100644 scripts/jsdoc_validator/jsdoc_validator.iml diff --git a/BUILD.gn b/BUILD.gn index daa3b1e4e8..86f1232bb6 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -167,6 +167,7 @@ generated_non_autostart_non_remote_modules = [ "$resources_out_dir/timeline_model/timeline_model_module.js", "$resources_out_dir/timeline/timeline_module.js", "$resources_out_dir/web_audio/web_audio_module.js", + "$resources_out_dir/dirac_lazy/dirac_lazy_module.js", "$resources_out_dir/webauthn/webauthn_module.js", "$resources_out_dir/workspace_diff/workspace_diff_module.js", "$resources_out_dir/emulated_devices/emulated_devices_module.js", diff --git a/DEPS b/DEPS index 06336eb3d9..0b5fb71608 100644 --- a/DEPS +++ b/DEPS @@ -96,7 +96,7 @@ hooks = [ '--no_auth', '--bucket', 'chromium-nodejs/12.14.1', '-s', 'third_party/node/linux/node-linux-x64.tar.gz.sha1', - ], + ], }, { 'name': 'node_mac', @@ -109,7 +109,7 @@ hooks = [ '--no_auth', '--bucket', 'chromium-nodejs/12.14.1', '-s', 'third_party/node/mac/node-darwin-x64.tar.gz.sha1', - ], + ], }, { 'name': 'node_win', @@ -121,7 +121,7 @@ hooks = [ '--no_auth', '--bucket', 'chromium-nodejs/12.14.1', '-s', 'third_party/node/win/node.exe.sha1', - ], + ], }, { @@ -130,9 +130,9 @@ hooks = [ 'name': 'disable_depot_tools_selfupdate', 'pattern': '.', 'action': [ - 'python', - 'third_party/depot_tools/update_depot_tools_toggle.py', - '--disable', + 'python', + 'third_party/depot_tools/update_depot_tools_toggle.py', + '--disable', ], }, @@ -147,7 +147,7 @@ hooks = [ '--no_auth', '--bucket', 'chromium-clang-format', '-s', 'buildtools/win/clang-format.exe.sha1', - ], + ], }, { 'name': 'clang_format_mac', @@ -159,7 +159,7 @@ hooks = [ '--no_auth', '--bucket', 'chromium-clang-format', '-s', 'buildtools/mac/clang-format.sha1', - ], + ], }, { 'name': 'clang_format_linux', @@ -171,46 +171,46 @@ hooks = [ '--no_auth', '--bucket', 'chromium-clang-format', '-s', 'buildtools/linux64/clang-format.sha1', - ], + ], }, # Pull chromium from common storage - { - 'name': 'download_chromium_win', - 'pattern': '.', - 'condition': 'host_os == "win"', - 'action': [ 'python', - 'scripts/deps/download_chromium.py', - 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win_x64/' + Var('chromium_win') + '/chrome-win.zip', - 'third_party/chrome', - 'chrome-win/chrome.exe', - Var('chromium_win'), - ], - }, - { - 'name': 'download_chromium_mac', - 'pattern': '.', - 'condition': 'host_os == "mac"', - 'action': [ 'python', - 'scripts/deps/download_chromium.py', - 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/' + Var('chromium_mac') + '/chrome-mac.zip', - 'third_party/chrome', - 'chrome-mac/Chromium.app/Contents', - Var('chromium_mac'), - ], - }, - { - 'name': 'download_chromium_linux', - 'pattern': '.', - 'condition': 'host_os == "linux"', - 'action': [ 'python', - 'scripts/deps/download_chromium.py', - 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/' + Var('chromium_linux') + '/chrome-linux.zip', - 'third_party/chrome', - 'chrome-linux/chrome', - Var('chromium_linux'), - ], - }, + # { + # 'name': 'download_chromium_win', + # 'pattern': '.', + # 'condition': 'host_os == "win"', + # 'action': [ 'python', + # 'scripts/deps/download_chromium.py', + # 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win_x64/' + Var('chromium_win') + '/chrome-win.zip', + # 'third_party/chrome', + # 'chrome-win/chrome.exe', + # Var('chromium_win'), + # ], + # }, + # { + # 'name': 'download_chromium_mac', + # 'pattern': '.', + # 'condition': 'host_os == "mac"', + # 'action': [ 'python', + # 'scripts/deps/download_chromium.py', + # 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/' + Var('chromium_mac') + '/chrome-mac.zip', + # 'third_party/chrome', + # 'chrome-mac/Chromium.app/Contents', + # Var('chromium_mac'), + # ], + # }, + # { + # 'name': 'download_chromium_linux', + # 'pattern': '.', + # 'condition': 'host_os == "linux"', + # 'action': [ 'python', + # 'scripts/deps/download_chromium.py', + # 'https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/' + Var('chromium_linux') + '/chrome-linux.zip', + # 'third_party/chrome', + # 'chrome-linux/chrome', + # Var('chromium_linux'), + # ], + # }, { 'name': 'sysroot_x64', 'pattern': '.', diff --git a/all_devtools_files.gni b/all_devtools_files.gni index ad26cf34b7..f9479c441f 100644 --- a/all_devtools_files.gni +++ b/all_devtools_files.gni @@ -5,6 +5,21 @@ import("./scripts/build/ninja/vars.gni") all_devtools_files = [ + # dirac - start + "front_end/console/clojure-parinfer.js", + "front_end/console/dirac-hacks.css", + "front_end/console/dirac-codemirror.css", + "front_end/console/dirac-theme.css", + "front_end/console/dirac-prompt.css", + "front_end/dirac/module.json", + "front_end/dirac/parinfer.js", + "front_end/dirac/parinfer-codemirror.js", + "front_end/dirac/keysim.js", + "front_end/dirac/dirac.js", + "front_end/dirac/require-implant.js", + "front_end/dirac_lazy/module.json", + "front_end/dirac_lazy/dirac_lazy.js", + # dirac - end "front_end/shell.js", "front_end/accessibility_test_runner/accessibility_test_runner.js", "front_end/accessibility_test_runner/module.json", diff --git a/all_devtools_modules.gni b/all_devtools_modules.gni index 8b3274dbca..9d3c1cb4a0 100644 --- a/all_devtools_modules.gni +++ b/all_devtools_modules.gni @@ -7,6 +7,14 @@ import("./scripts/build/ninja/vars.gni") generated_typescript_modules = [] all_typescript_module_sources = [ + # dirac - start +# "dirac/parinfer.js", +# "dirac/parinfer-codemirror.js", +# "dirac/keysim.js", + "third_party/codemirror/package/addon/runmode/runmode.js", + "third_party/codemirror/package/addon/display/placeholder.js", + "console/ConsoleDiracPrompt.js", + # dirac - end "accessibility/ARIAAttributesView.js", "accessibility/ARIAMetadata.js", "accessibility/AXBreadcrumbsPane.js", diff --git a/devtools.iml b/devtools.iml new file mode 100644 index 0000000000..0f67e2feaa --- /dev/null +++ b/devtools.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/devtools_grd_files.gni b/devtools_grd_files.gni index 10b79ec26a..51d9bb5587 100644 --- a/devtools_grd_files.gni +++ b/devtools_grd_files.gni @@ -10,6 +10,13 @@ # are missed from the GRD. grd_files_release_sources = [ + # dirac - start +# "front_end/dirac/dirac.js", +# "front_end/dirac/parinfer.js", +# "front_end/dirac/parinfer-codemirror.js", +# "front_end/dirac/keysim.js", +# "front_end/dirac_lazy/dirac_lazy_module.js", + # dirac - end "front_end/Images/accelerometer-back.svg", "front_end/Images/accelerometer-bottom.png", "front_end/Images/accelerometer-front.svg", @@ -448,6 +455,7 @@ grd_files_debug_sources = [ "front_end/console/ConsolePanel.js", "front_end/console/ConsolePinPane.js", "front_end/console/ConsolePrompt.js", + "front_end/console/ConsoleDiracPrompt.js", "front_end/console/ConsoleSidebar.js", "front_end/console/ConsoleView.js", "front_end/console/ConsoleViewMessage.js", @@ -904,6 +912,10 @@ grd_files_debug_sources = [ "front_end/third_party/codemirror/package/addon/runmode/runmode-standalone.js", "front_end/third_party/codemirror/package/addon/selection/active-line.js", "front_end/third_party/codemirror/package/addon/selection/mark-selection.js", + # dirac - start + "front_end/third_party/codemirror/package/addon/runmode/runmode.js", + "front_end/third_party/codemirror/package/addon/display/placeholder.js", + # dirac - end "front_end/third_party/codemirror/package/lib/codemirror.js", "front_end/third_party/codemirror/package/mode/clike/clike.js", "front_end/third_party/codemirror/package/mode/clojure/clojure.js", diff --git a/front_end/accessibility/ARIAAttributesView.js b/front_end/accessibility/ARIAAttributesView.js index 43b76f71c5..f6deef84c9 100644 --- a/front_end/accessibility/ARIAAttributesView.js +++ b/front_end/accessibility/ARIAAttributesView.js @@ -1,3 +1,4 @@ +// @ts-nocheck // 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. diff --git a/front_end/bindings/ResourceScriptMapping.js b/front_end/bindings/ResourceScriptMapping.js index 713914c5bb..7b1c8a138e 100644 --- a/front_end/bindings/ResourceScriptMapping.js +++ b/front_end/bindings/ResourceScriptMapping.js @@ -297,6 +297,13 @@ export class ResourceScriptFile extends Common.ObjectWrapper.ObjectWrapper { Workspace.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); } + /** + * @return {?SDK.Script.Script} + */ + getScript() { + return this._script; + } + /** * @param {!Array.} scripts * @return {boolean} diff --git a/front_end/cm/cm.js b/front_end/cm/cm.js index cc20e75462..d39ab2da65 100644 --- a/front_end/cm/cm.js +++ b/front_end/cm/cm.js @@ -14,3 +14,7 @@ import '../third_party/codemirror/package/addon/selection/mark-selection.js'; import '../third_party/codemirror/package/addon/fold/foldcode.js'; import '../third_party/codemirror/package/addon/fold/foldgutter.js'; import '../third_party/codemirror/package/addon/fold/brace-fold.js'; + +// for dirac +import '../third_party/codemirror/package/addon/runmode/runmode.js'; +import '../third_party/codemirror/package/addon/display/placeholder.js'; diff --git a/front_end/cm/module.json b/front_end/cm/module.json index 8630421bc7..0b5373aef8 100644 --- a/front_end/cm/module.json +++ b/front_end/cm/module.json @@ -12,7 +12,9 @@ "../third_party/codemirror/package/addon/selection/mark-selection.js", "../third_party/codemirror/package/addon/fold/foldcode.js", "../third_party/codemirror/package/addon/fold/foldgutter.js", - "../third_party/codemirror/package/addon/fold/brace-fold.js" + "../third_party/codemirror/package/addon/fold/brace-fold.js", + "../third_party/codemirror/package/addon/runmode/runmode.js", + "../third_party/codemirror/package/addon/display/placeholder.js" ], "skip_compilation": [ "codemirror.js", @@ -27,7 +29,9 @@ "../third_party/codemirror/package/addon/selection/mark-selection.js", "../third_party/codemirror/package/addon/fold/foldcode.js", "../third_party/codemirror/package/addon/fold/foldgutter.js", - "../third_party/codemirror/package/addon/fold/brace-fold.js" + "../third_party/codemirror/package/addon/fold/brace-fold.js", + "../third_party/codemirror/package/addon/runmode/runmode.js", + "../third_party/codemirror/package/addon/display/placeholder.js" ], "resources": [ "codemirror.css" ] diff --git a/front_end/common/Settings.js b/front_end/common/Settings.js index 4e06914a52..0a66d85c4d 100644 --- a/front_end/common/Settings.js +++ b/front_end/common/Settings.js @@ -441,7 +441,7 @@ export class RegExpSetting extends Setting { * @param {string=} regexFlags */ constructor(settings, name, defaultValue, eventSupport, storage, regexFlags) { - super(settings, name, defaultValue ? [{pattern: defaultValue}] : [], eventSupport, storage); + super(settings, name, defaultValue ? (typeof defaultValue === 'string' ? [{pattern: defaultValue}] : defaultValue) : [], eventSupport, storage); this._regexFlags = regexFlags; } diff --git a/front_end/components/Linkifier.js b/front_end/components/Linkifier.js index 88f6d149cd..6537090017 100644 --- a/front_end/components/Linkifier.js +++ b/front_end/components/Linkifier.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* * Copyright (C) 2012 Google Inc. All rights reserved. * @@ -827,6 +828,7 @@ export class Linkifier { } if (contentProvider) { const lineNumber = uiLocation ? uiLocation.lineNumber : info.lineNumber || 0; + const columnNumber = uiLocation ? uiLocation.columnNumber : info.columnNumber || 0; for (const title of linkHandlers.keys()) { const handler = linkHandlers.get(title); if (!handler) { @@ -843,6 +845,15 @@ export class Linkifier { result.push(action); } } + if (dirac.hasLinkActions) { + const diracAction = Components.Linkifier.diracLinkHandlerAction; + if (diracAction) { + result.unshift({ + title: diracAction.title, + handler: diracAction.handler.bind(null, result, contentProvider.contentURL(), lineNumber, columnNumber) + }); + } + } } if (resource || info.url) { result.push({ diff --git a/front_end/components/components-legacy.js b/front_end/components/components-legacy.js index bcc7a87d41..6802567938 100644 --- a/front_end/components/components-legacy.js +++ b/front_end/components/components-legacy.js @@ -29,6 +29,9 @@ Components.Linkifier.LinkHandlerSettingUI = ComponentsModule.Linkifier.LinkHandl /** @constructor */ Components.Linkifier.ContentProviderContextMenuProvider = ComponentsModule.Linkifier.ContentProviderContextMenuProvider; +/** @type {?Object} */ +Components.Linkifier.diracLinkHandlerAction = null; + /** @interface */ Components.LinkDecorator = ComponentsModule.Linkifier.LinkDecorator; diff --git a/front_end/console/BUILD.gn b/front_end/console/BUILD.gn index d4ef94720a..1d2db0cf66 100644 --- a/front_end/console/BUILD.gn +++ b/front_end/console/BUILD.gn @@ -12,6 +12,7 @@ devtools_module("console") { "ConsolePanel.js", "ConsolePinPane.js", "ConsolePrompt.js", + "ConsoleDiracPrompt.js", "ConsoleSidebar.js", "ConsoleView.js", "ConsoleViewMessage.js", diff --git a/front_end/console/ConsoleDiracPrompt.js b/front_end/console/ConsoleDiracPrompt.js new file mode 100644 index 0000000000..5dfc76a409 --- /dev/null +++ b/front_end/console/ConsoleDiracPrompt.js @@ -0,0 +1,953 @@ +// @ts-nocheck +// Copyright 2020 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 {ConsoleHistoryManager} from './ConsolePrompt.js'; +import * as UI from '../ui/ui.js'; + +/** + * @unrestricted + */ +export class ConsoleDiracPrompt extends UI.TextPrompt.TextPrompt { + + /** + * @param {!CodeMirror} codeMirrorInstance + */ + constructor(codeMirrorInstance) { + super(); + + this._history = new ConsoleHistoryManager(); + this._codeMirror = codeMirrorInstance; + this._codeMirror.on('changes', this._changes.bind(this)); + this._codeMirror.on('scroll', this._onScroll.bind(this)); + this._codeMirror.on('cursorActivity', this._onCursorActivity.bind(this)); + this._codeMirror.on('blur', this._blur.bind(this)); + this._currentClojureScriptNamespace = null; + this._lastAutocompleteRequest = 0; + // just to mimic disabled eager preview functionality of ConsolePrompt + this._eagerPreviewElement = document.createElement('div'); + this._eagerPreviewElement.classList.add('console-eager-preview'); + } + + /** + * @return {!Element} + */ + // just to mimic disabled eager preview functionality of ConsolePrompt, see https://github.com/binaryage/dirac/issues/78 + belowEditorElement() { + return this._eagerPreviewElement; + } + + /** + * @return {!ConsoleHistoryManager} + */ + history() { + return this._history; + } + + /** + * @return {boolean} + */ + hasFocus() { + return this._codeMirror.hasFocus(); + } + + /** + * @override + */ + focus() { + this._codeMirror.focus(); + // HACK: this is needed to properly display cursor in empty codemirror: + // http://stackoverflow.com/questions/10575833/codemirror-has-content-but-wont-display-until-keypress + this._codeMirror.refresh(); + } + + setCurrentClojureScriptNamespace(ns) { + this._currentClojureScriptNamespace = ns; + } + + /** + * @override + * @return {string} + */ + text() { + const text = this._codeMirror.getValue(); + return text.replace(/[\s\n]+$/gm, ''); // remove trailing newlines and whitespace + } + + /** + * @override + * @param {string} x + */ + setText(x) { + this.clearAutocomplete(); + this._codeMirror.setValue(x); + this.moveCaretToEndOfPrompt(); + this._element.scrollIntoView(); + } + + /** + * @return {boolean} + */ + _isSuggestBoxVisible() { + if (this._suggestBox) { + return this._suggestBox.visible(); + } + return false; + + } + + /** + * @override + * @return {boolean} + */ + isCaretInsidePrompt() { + return this._codeMirror.hasFocus(); + } + + /** + * @override + * @return {boolean} + */ + _isCaretAtEndOfPrompt() { + const content = this._codeMirror.getValue(); + const cursor = this._codeMirror.getCursor(); + const endCursor = this._codeMirror.posFromIndex(content.length); + return (cursor.line === endCursor.line && cursor.ch === endCursor.ch); + } + + /** + * @return {boolean} + */ + isCaretOnFirstLine() { + const cursor = this._codeMirror.getCursor(); + return (cursor.line === this._codeMirror.firstLine()); + } + + /** + * @return {boolean} + */ + isCaretOnLastLine() { + const cursor = this._codeMirror.getCursor(); + return (cursor.line === this._codeMirror.lastLine()); + } + + + /** + * @override + */ + moveCaretToEndOfPrompt() { + this._codeMirror.setCursor(this._codeMirror.lastLine() + 1, 0, null); + } + + /** + * @override + */ + moveCaretToIndex(index) { + const pos = this._codeMirror.posFromIndex(index); + this._codeMirror.setCursor(pos, null, null); + } + + finishAutocomplete() { + if (dirac.DEBUG_COMPLETIONS) { + console.log('finishAutocomplete', (new Error()).stack); + } + this.clearAutocomplete(); + this._prefixRange = null; + this._anchorBox = null; + } + + /** + * @param {!CodeMirror} codeMirror + * @param {!Array.} changes + */ + _changes(codeMirror, changes) { + if (!changes.length) { + return; + } + + let singleCharInput = false; + for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) { + const changeObject = changes[changeIndex]; + singleCharInput = (changeObject.origin === '+input' && changeObject.text.length === 1 && changeObject.text[0].length === 1) || + (this._isSuggestBoxVisible() && changeObject.origin === '+delete' && changeObject.removed.length === 1 && changeObject.removed[0].length === 1); + } + if (dirac.DEBUG_COMPLETIONS) { + console.log('_changes', singleCharInput, changes); + } + if (singleCharInput) { + this._ignoreNextCursorActivity = true; // this prevents flickering of suggestion widget + // noinspection JSUnresolvedFunction + setImmediate(this.autocomplete.bind(this)); + } + } + + _blur() { + this.finishAutocomplete(); + } + + _onScroll() { + if (!this._isSuggestBoxVisible()) { + return; + } + + const cursor = this._codeMirror.getCursor(); + const scrollInfo = this._codeMirror.getScrollInfo(); + const topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, 'local'); + const bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local'); + if (cursor.line < topmostLineNumber || cursor.line > bottomLine) { + this.finishAutocomplete(); + } else { + this._updateAnchorBox(); + this._suggestBox.setPosition(this._anchorBox); + } + } + + _onCursorActivity() { + if (!this._isSuggestBoxVisible()) { + return; + } + + if (this._ignoreNextCursorActivity) { + delete this._ignoreNextCursorActivity; + return; + } + + const cursor = this._codeMirror.getCursor(); + if (this._prefixRange) { + if (cursor.line !== this._prefixRange.startLine || + cursor.ch > this._prefixRange.endColumn || + cursor.ch <= this._prefixRange.startColumn) { + this.finishAutocomplete(); + } + } else { + console.log('_prefixRange nil (unexpected)', (new Error()).stack); + } + } + + /** + * @override + * @param {boolean=} force + */ + async complete(force) { + // override with empty implementation to disable TextPrompt's autocomplete implementation + // we use CodeMirror's changes modelled after TextEditorAutocompleteController.js in DiracPrompt + if (dirac.DEBUG_COMPLETIONS) { + console.log('complete called => skip for disabling default auto-complete system'); + } + } + + /** + * @override + * @param {boolean=} force + */ + autoCompleteSoon(force) { + this._ignoreNextCursorActivity = true; // this prevents flickering of suggestion widget + // noinspection JSUnresolvedFunction + setImmediate(this.autocomplete.bind(this)); + } + + /** + * @override + * @param {string} prefix + * @return {!UI.SuggestBox.Suggestions} + */ + additionalCompletions(prefix) { + // we keep this list empty for now, history contains mostly cljs stuff and we don't want to mix it with javascript + return []; + } + + _javascriptCompletionTest(prefix) { + // test if prefix starts with "js/", then we treat it as javascript completion + const m = prefix.match(/^js\/(.*)/); + if (m) { + return { + prefix: m[1], + offset: 3 + }; + } + } + + /** + * @param {boolean=} force + * @param {boolean=} reverse + */ + autocomplete(force, reverse) { + force = force || false; + reverse = reverse || false; + this.clearAutocomplete(); + this._lastAutocompleteRequest++; + + let shouldExit = false; + const cursor = this._codeMirror.getCursor(); + const token = this._codeMirror.getTokenAt(cursor); + + if (dirac.DEBUG_COMPLETIONS) { + console.log('autocomplete:', cursor, token); + } + + if (!token) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('no autocomplete because no token'); + } + shouldExit = true; + } else if (this._codeMirror.somethingSelected()) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('no autocomplete because codeMirror.somethingSelected()'); + } + shouldExit = true; + } else if (!force) { + if (token.end !== cursor.ch) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('no autocomplete because cursor is not at the end of detected token'); + } + shouldExit = true; + } + } + + if (shouldExit) { + this.clearAutocomplete(); + return; + } + + const prefix = this._codeMirror.getRange(new CodeMirror.Pos(cursor.line, token.start), cursor); + const javascriptCompletion = this._javascriptCompletionTest(prefix); + if (dirac.DEBUG_COMPLETIONS) { + console.log("detected prefix='" + prefix + "'", javascriptCompletion); + } + if (javascriptCompletion) { + this._prefixRange = new TextUtils.TextRange(cursor.line, token.start + javascriptCompletion.offset, cursor.line, cursor.ch); + const completionsForJavascriptReady = this._completionsForJavascriptReady.bind(this, this._lastAutocompleteRequest, reverse, force); + this._loadJavascriptCompletions(this._lastAutocompleteRequest, javascriptCompletion.prefix, force, completionsForJavascriptReady); + } else { + this._prefixRange = new TextUtils.TextRange(cursor.line, token.start, cursor.line, cursor.ch); + const completionsForClojureScriptReady = this._completionsForClojureScriptReady.bind(this, this._lastAutocompleteRequest, reverse, force); + this._loadClojureScriptCompletions(this._lastAutocompleteRequest, prefix, force, completionsForClojureScriptReady); + } + } + + /** + * @param {number} requestId + * @param {string} input + * @param {boolean} force + * @param {function(string, string, !UI.SuggestBox.Suggestions)} completionsReadyCallback + */ + _loadJavascriptCompletions(requestId, input, force, completionsReadyCallback) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_loadJavascriptCompletions', input, force); + } + if (requestId !== this._lastAutocompleteRequest) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_loadJavascriptCompletions cancelled', requestId, this._lastAutocompleteRequest); + } + return; + } + + let prefix = input; + let expressionString = ''; + const lastDotIndex = input.lastIndexOf('.'); + const lastOpenSquareBracketIndex = input.lastIndexOf('['); + + if (lastOpenSquareBracketIndex > lastDotIndex) { + // split at last square bracket + expressionString = input.substring(0, lastOpenSquareBracketIndex + 1); + prefix = input.substring(lastOpenSquareBracketIndex + 1); + } else { + if (lastDotIndex >= 0) { + // split at last dot + expressionString = input.substring(0, lastDotIndex + 1); + prefix = input.substring(lastDotIndex + 1); + } + } + + ObjectUI.javaScriptAutocomplete.completionsForTextInCurrentContext(expressionString, prefix, force).then(completionsReadyCallback.bind(this, expressionString, prefix)); + } + + /** + * @param {number} requestId + * @param {boolean} reverse + * @param {boolean} force + * @param {string} expression + * @param {string} prefix + * @param {!UI.SuggestBox.Suggestions} completions + */ + _completionsForJavascriptReady(requestId, reverse, force, expression, prefix, completions) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_completionsForJavascriptReady', prefix, reverse, force, expression, completions); + } + if (requestId !== this._lastAutocompleteRequest) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_completionsForJavascriptReady cancelled', requestId, this._lastAutocompleteRequest); + } + return; + } + + // Filter out dupes. + const store = new Set(); + completions = completions.filter(item => !store.has(item.text) && !!store.add(item.text)); + + if (!completions.length) { + this.clearAutocomplete(); + return; + } + + this._userEnteredText = prefix; + + this._lastExpression = expression; + this._updateAnchorBox(); + + const shouldShowForSingleItem = true; // later maybe implement inline completions like in TextPrompt.js + if (this._anchorBox) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('calling SuggestBox.updateSuggestions', this._anchorBox, completions, shouldShowForSingleItem, this._userEnteredText); + } + this._suggestBox.updateSuggestions(this._anchorBox, completions, true, shouldShowForSingleItem, this._userEnteredText); + } else { + if (dirac.DEBUG_COMPLETIONS) { + console.log('not calling SuggestBox.updateSuggestions because this._anchorBox is null', completions, shouldShowForSingleItem, this._userEnteredText); + } + } + + // here could be implemented inline completions like in TextPrompt.js + } + + /** + * @param {number} requestId + * @param {string} input + * @param {boolean} force + * @param {function(string, string, !Array., number=)} completionsReadyCallback + */ + _loadClojureScriptCompletions(requestId, input, force, completionsReadyCallback) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_loadClojureScriptCompletions', input, force); + } + if (requestId !== this._lastAutocompleteRequest) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_loadClojureScriptCompletions cancelled', requestId, this._lastAutocompleteRequest); + } + return; + } + const executionContext = self.UI.context.flavor(SDK.ExecutionContext); + if (!executionContext) { + if (dirac.DEBUG_COMPLETIONS) { + console.warn('no execution context available'); + } + completionsReadyCallback('', '', []); + return; + } + + const debuggerModel = executionContext.debuggerModel; + if (!debuggerModel) { + if (dirac.DEBUG_COMPLETIONS) { + console.warn('no debugger model available'); + } + completionsReadyCallback('', '', []); + return; + } + + const makeSuggestStyle = (style = '') => `suggest-cljs ${style}`; + + const namespaceSelector = name => { + return function (namespaceDescriptors) { + return namespaceDescriptors[name]; + }; + }; + const selectCurrentNamespace = namespaceSelector(this._currentClojureScriptNamespace); + + const concatAnnotatedResults = results => { + return [].concat.apply([], results); + }; + + const lastSlashIndex = input.lastIndexOf('/'); + if (lastSlashIndex >= 0) { + // completion of fully qualified name => split at last slash + // example for input = "some.namespace/some-sym": + // prefix <= "some-sym" + // expression <= "some.namespace/" + // namespace <= "some.namespace" + // + // present only symbols from given namespace, matching given prefix + // note that some.namespace may be also alias to a namespace or a macro namespace, we will resolve it + + const prefix = input.substring(lastSlashIndex + 1); + const expression = input.substring(0, lastSlashIndex + 1); + const namespace = input.substring(0, lastSlashIndex); + + const annotateQualifiedSymbols = (style, symbols) => { + return symbols.filter(symbol => symbol.startsWith(prefix)).map(symbol => ({ + text: symbol || '?', + className: makeSuggestStyle(style) + })); + }; + + const styleQualifiedSymbols = (style, symbols) => { + return symbols.filter(symbol => symbol.text.startsWith(prefix)).map(symbol => { + symbol.className = makeSuggestStyle(style); + return symbol; + }); + }; + + const currentNamespaceDescriptorPromise = dirac.extractNamespacesAsync().then(selectCurrentNamespace); + + const resolvedNamespaceNamePromise = currentNamespaceDescriptorPromise.then(currentNamespaceDescriptor => { + if (!currentNamespaceDescriptor) { + return namespace; + } + const namespaceAliases = currentNamespaceDescriptor.namespaceAliases || {}; + const macroNamespaceAliases = currentNamespaceDescriptor.macroNamespaceAliases || {}; + const allAliases = Object.assign({}, namespaceAliases, macroNamespaceAliases); + return allAliases[namespace] || namespace; // resolve alias or assume namespace name is a full namespace name + }); + + const prepareAnnotatedJavascriptCompletionsForPseudoNamespaceAsync = namespaceName => { + return new Promise(resolve => { + const resultHandler = (expression, prefix, completions) => { + const annotatedCompletions = styleQualifiedSymbols('suggest-cljs-qualified suggest-cljs-pseudo', completions); + if (dirac.DEBUG_COMPLETIONS) { + console.log('resultHandler got', expression, prefix, completions, annotatedCompletions); + } + resolve(annotatedCompletions); + }; + + this._loadJavascriptCompletions(requestId, namespaceName + '.', force, resultHandler); + }); + }; + + const readyCallback = completionsReadyCallback.bind(this, expression, prefix); + + const provideCompletionsForNamespace = ([namespaces, namespaceName]) => { + const namespace = namespaces[namespaceName]; + if (!namespace) { + const macroNamespaceNames = dirac.getMacroNamespaceNames(namespaces); + if (!macroNamespaceNames.includes(namespaceName)) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('no known namespace for ', namespaceName); + } + readyCallback([]); + return; + } + if (dirac.DEBUG_COMPLETIONS) { + console.log('namespace is a macro namespace', namespaceName); + } + + } + + if (namespace && namespace.pseudo) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('pseudo namespace => falling back to JS completions', namespaceName); + } + prepareAnnotatedJavascriptCompletionsForPseudoNamespaceAsync(namespaceName).then(readyCallback); + return; + } + + if (dirac.DEBUG_COMPLETIONS) { + console.log('cljs namespace => retrieving symbols and macros from caches', namespaceName); + } + const namespaceSymbolsPromise = dirac.extractNamespaceSymbolsAsync(namespaceName) + .then(annotateQualifiedSymbols.bind(this, 'suggest-cljs-qualified')); + const macroNamespaceSymbolsPromise = dirac.extractMacroNamespaceSymbolsAsync(namespaceName) + .then(annotateQualifiedSymbols.bind(this, 'suggest-cljs-qualified suggest-cljs-macro')); + + // order matters here, see _markAliasedCompletions below + const jobs = [ + namespaceSymbolsPromise, + macroNamespaceSymbolsPromise + ]; + + Promise.all(jobs).then(concatAnnotatedResults).then(readyCallback); + }; + + Promise.all([dirac.extractNamespacesAsync(), resolvedNamespaceNamePromise]).then(provideCompletionsForNamespace.bind(this)); + } else { + // general completion (without slashes) + // combine: locals (if paused in debugger), current ns symbols, namespace names and cljs.core symbols + // filter the list by input prefix + + const annotateSymbols = (style, symbols) => { + return symbols.filter(symbol => symbol.startsWith(input)).map(symbol => ({ + text: symbol || '?', + className: makeSuggestStyle(style) + })); + }; + + /** + * @param {dirac.ScopeInfo} scopeInfo + * @return {!Array} + */ + const extractLocalsFromScopeInfo = scopeInfo => { + const locals = []; + if (!scopeInfo) { + return locals; + } + + const frames = scopeInfo.frames; + if (frames) { + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]; + const props = frame.props; + + if (props) { + for (let j = 0; j < props.length; j++) { + const prop = props[j]; + locals.push(prop); + } + } + } + } + + // deduplicate + const keyFn = item => '' + item.name + item.identifier; + const store = new Set(); + return locals.filter(item => !store.has(keyFn(item)) && !!store.add(keyFn(item))); + }; + + const extractAndAnnotateLocals = scopeInfo => { + const locals = extractLocalsFromScopeInfo(scopeInfo); + const filteredLocals = locals.filter(item => item.name.startsWith(input)); + const annotatedCompletions = filteredLocals.map(item => ({ + text: item.name || '?', + epilogue: item.identifier ? 'js/' + item.identifier : undefined, + className: makeSuggestStyle('suggest-cljs-scope') + })); + annotatedCompletions.reverse(); // we want to display inner scopes first + return annotatedCompletions; + }; + + const annotateNamespaceName = namespace => { + let extraStyle = ''; + if (namespace.pseudo) { + extraStyle += ' suggest-cljs-pseudo'; + } + return { + text: namespace.name || '?', + className: makeSuggestStyle('suggest-cljs-ns' + extraStyle) + }; + }; + + const annotateNamespaceNames = namespaces => { + return Object.keys(namespaces) + .filter(name => name.startsWith(input)) + .map(name => annotateNamespaceName(namespaces[name])); + }; + + const annotateMacroNamespaceNames = namespaces => { + return namespaces.filter(name => name.startsWith(input)).map(name => ({ + text: name || '?', + className: makeSuggestStyle('suggest-cljs-ns suggest-cljs-macro') + })); + }; + + const annotateAliasesOrRefers = (kind, prefix, style, namespaceDescriptor) => { + if (!namespaceDescriptor) { + return []; + } + + return dirac.extractNamespacesAsync().then(namespaces => { + const mapping = namespaceDescriptor[kind] || {}; + return Object.keys(mapping).filter(name => name.startsWith(input)).map(name => { + const targetName = mapping[name]; + const targetNamespace = namespaces[targetName] || {}; + let extraStyle = ''; + if (targetNamespace.pseudo) { + extraStyle += ' suggest-cljs-pseudo'; + } + return { + text: name, + epilogue: targetName ? prefix + targetName : null, // full target name + className: makeSuggestStyle(style + extraStyle) + }; + }); + + }); + }; + + const annotateReplSpecials = symbols => { + return symbols.filter(symbol => symbol.startsWith(input)).map(symbol => ({ + text: symbol || '?', + className: makeSuggestStyle('suggest-cljs-repl suggest-cljs-special') + })); + }; + + const localsPromise = dirac.extractScopeInfoFromScopeChainAsync(debuggerModel.selectedCallFrame()).then(extractAndAnnotateLocals); + const currentNamespaceSymbolsPromise = dirac.extractNamespaceSymbolsAsync(this._currentClojureScriptNamespace).then(annotateSymbols.bind(this, 'suggest-cljs-in-ns')); + const namespaceNamesPromise = dirac.extractNamespacesAsync().then(annotateNamespaceNames); + const macroNamespaceNamesPromise = dirac.extractNamespacesAsync().then(dirac.getMacroNamespaceNames).then(annotateMacroNamespaceNames); + const coreNamespaceSymbolsPromise = dirac.extractNamespaceSymbolsAsync('cljs.core').then(annotateSymbols.bind(this, 'suggest-cljs-core')); + const currentNamespaceDescriptor = dirac.extractNamespacesAsync().then(selectCurrentNamespace); + const namespaceAliasesPromise = currentNamespaceDescriptor.then(annotateAliasesOrRefers.bind(this, 'namespaceAliases', 'is ', 'suggest-cljs-ns-alias')); + const macroNamespaceAliasesPromise = currentNamespaceDescriptor.then(annotateAliasesOrRefers.bind(this, 'macroNamespaceAliases', 'is ', 'suggest-cljs-ns-alias suggest-cljs-macro')); + const namespaceRefersPromise = currentNamespaceDescriptor.then(annotateAliasesOrRefers.bind(this, 'namespaceRefers', 'in ', 'suggest-cljs-refer')); + const macroRefersPromise = currentNamespaceDescriptor.then(annotateAliasesOrRefers.bind(this, 'macroRefers', 'in ', 'suggest-cljs-refer suggest-cljs-macro')); + const replSpecialsPromise = dirac.getReplSpecialsAsync().then(annotateReplSpecials); + + // order matters here, see _markAliasedCompletions below + const jobs = [ + replSpecialsPromise, + localsPromise, + currentNamespaceSymbolsPromise, + namespaceRefersPromise, + macroRefersPromise, + namespaceAliasesPromise, + macroNamespaceAliasesPromise, + namespaceNamesPromise, + macroNamespaceNamesPromise, + coreNamespaceSymbolsPromise + ]; + + Promise.all(jobs).then(concatAnnotatedResults).then(completionsReadyCallback.bind(this, '', input)); + } + } + + /** + * @param {number} requestId + * @param {boolean} reverse + * @param {boolean} force + * @param {string} expression + * @param {string} prefix + * @param {!Array.} completions + */ + _completionsForClojureScriptReady(requestId, reverse, force, expression, prefix, completions) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_completionsForClojureScriptReady', prefix, reverse, force, completions); + } + + if (requestId !== this._lastAutocompleteRequest) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('_loadClojureScriptCompletions cancelled', requestId, this._lastAutocompleteRequest); + } + return; + } + + const sortCompletions = completions => { + return dirac.stableSort(completions, (a, b) => { + return a.text.localeCompare(b.text); + }); + }; + + const markAliasedCompletions = annotatedCompletions => { + let previous = null; + for (const current of annotatedCompletions) { + if (previous) { + if (current.text === previous.text) { + if (!current.className) { + current.className = 'suggest-cljs-aliased'; + } else { + current.className += ' suggest-cljs-aliased'; + } + } + } + previous = current; + } + return annotatedCompletions; + }; + + const combineAliasedMacroNamespacesInCompletions = completions => { + const result = []; + let previous = null; + for (const current of completions) { + let skip = false; + if (previous) { + if (current.text === previous.text) { + if (previous.className.includes('suggest-cljs-ns') && + current.className.includes('suggest-cljs-ns') && + current.className.includes('suggest-cljs-macro')) { + skip = true; + previous.className += ' suggest-cljs-macro suggest-cljs-combined-ns-macro'; + } + } + } + previous = current; + if (!skip) { + result.push(current); + } + } + return result; + }; + + const processedCompletions = combineAliasedMacroNamespacesInCompletions(markAliasedCompletions(sortCompletions(completions))); + + if (!processedCompletions.length) { + this.clearAutocomplete(); + return; + } + + this._userEnteredText = prefix; + + if (this._suggestBox) { + this._lastExpression = expression; + this._updateAnchorBox(); + const shouldShowForSingleItem = true; // later maybe implement inline completions like in TextPrompt.js + if (this._anchorBox) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('calling SuggestBox.updateSuggestions', this._anchorBox, processedCompletions, shouldShowForSingleItem, this._userEnteredText); + } + this._suggestBox.updateSuggestions(this._anchorBox, processedCompletions, true, shouldShowForSingleItem, this._userEnteredText); + } else { + if (dirac.DEBUG_COMPLETIONS) { + console.log('not calling SuggestBox.updateSuggestions because this._anchorBox is null', processedCompletions, shouldShowForSingleItem, this._userEnteredText); + } + } + } + + // here could be implemented inline completions like in TextPrompt.js + } + + + _updateAnchorBox() { + let metrics; + if (this._prefixRange) { + const line = this._prefixRange.startLine; + const column = this._prefixRange.startColumn; + metrics = this.cursorPositionToCoordinates(line, column); + } else { + console.log('_prefixRange nil (unexpected)', (new Error()).stack); + metrics = this.cursorPositionToCoordinates(0, 0); + } + this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null; + } + + /** + * @param {number} lineNumber + * @param {number} column + * @return {?{x: number, y: number, height: number}} + */ + cursorPositionToCoordinates(lineNumber, column) { + if (lineNumber >= this._codeMirror.lineCount() || lineNumber < 0 || column < 0 || column > this._codeMirror.getLine(lineNumber).length) { + return null; + } + + const metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, column)); + + return { + x: metrics.left, + y: metrics.top, + height: metrics.bottom - metrics.top + }; + } + + /** + * @override + * @param {?UI.SuggestBox.Suggestion} suggestion + * @param {boolean=} isIntermediateSuggestion + */ + applySuggestion(suggestion, isIntermediateSuggestion) { + if (dirac.DEBUG_COMPLETIONS) { + console.log('applySuggestion', this._lastExpression, suggestion); + } + const suggestionText = suggestion ? suggestion.text : ''; + this._currentSuggestion = this._lastExpression + suggestionText; + } + + /** + * @override + */ + acceptSuggestion() { + if (!this._prefixRange) { + console.log('_prefixRange nil (unexpected)', (new Error()).stack); + return; + } + if (this._prefixRange.endColumn - this._prefixRange.startColumn === this._currentSuggestion.length) { + return; + } + + const selections = this._codeMirror.listSelections().slice(); + if (dirac.DEBUG_COMPLETIONS) { + console.log('acceptSuggestion', this._prefixRange, selections); + } + const prefixLength = this._prefixRange.endColumn - this._prefixRange.startColumn; + for (let i = selections.length - 1; i >= 0; --i) { + const start = selections[i].head; + const end = new CodeMirror.Pos(start.line, start.ch - prefixLength); + this._codeMirror.replaceRange(this._currentSuggestion, start, end, '+autocomplete'); + } + } + + /** + * @override + */ + _acceptSuggestionInternal() { + } + + /** + * @override + * @return {string} + */ + getSuggestBoxRepresentation() { + if (!this._suggestBox || !this._suggestBox.visible()) { + return 'suggest box is not visible'; + } + const res = ['suggest box displays ' + this._suggestBox._list._model.length + ' items:']; + + const children = this._suggestBox._element.children; + for (const child of children) { + res.push(' * ' + child.textContent); + } + + return res.join('\n'); + } + + /** + * @param {!TextUtils.TextRange} textRange + */ + setSelection(textRange) { + this._lastSelection = textRange; + const pos = TextEditor.CodeMirrorUtils.toPos(textRange); + this._codeMirror.setSelection(pos.start, pos.end, {}); + } + + /** + * @override + */ + onKeyDown(event) { + let newText; + let isPrevious; + + switch (event.keyCode) { + case UI.KeyboardShortcut.Keys.Up.code: + if (!this.isCaretOnFirstLine() || this._isSuggestBoxVisible()) { + break; + } + newText = this._history.previous(this.text()); + isPrevious = true; + break; + case UI.KeyboardShortcut.Keys.Down.code: + if (!this.isCaretOnLastLine() || this._isSuggestBoxVisible()) { + break; + } + newText = this._history.next(); + break; + case UI.KeyboardShortcut.Keys.P.code: // Ctrl+P = Previous + if (Host.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { + newText = this._history.previous(this.text()); + isPrevious = true; + } + break; + case UI.KeyboardShortcut.Keys.N.code: // Ctrl+N = Next + if (Host.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { + newText = this._history.next(); + } + break; + } + + if (newText !== undefined) { + event.consume(true); + this.setText(newText); + this.clearAutocomplete(); + + if (isPrevious) { + this.setSelection(TextUtils.TextRange.createFromLocation(0, Infinity)); + } else { + this.moveCaretToEndOfPrompt(); + } + + return; + } + + try { + dirac.ignoreEnter = true; // a workaround for https://github.com/binaryage/dirac/issues/72 + UI.TextPrompt.TextPrompt.prototype.onKeyDown.apply(this, arguments); + } finally { + dirac.ignoreEnter = false; + } + } +} diff --git a/front_end/console/ConsoleView.js b/front_end/console/ConsoleView.js index d407239e71..ffd8c67951 100644 --- a/front_end/console/ConsoleView.js +++ b/front_end/console/ConsoleView.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2009 Joseph Pecoraro @@ -44,6 +45,7 @@ import {ConsolePrompt, Events as ConsolePromptEvents} from './ConsolePrompt.js'; import {ConsoleSidebar, Events} from './ConsoleSidebar.js'; import {ConsoleCommand, ConsoleCommandResult, ConsoleGroupViewMessage, ConsoleTableMessageView, ConsoleViewMessage, getMessageForElement, MaxLengthForLinks} from './ConsoleViewMessage.js'; // eslint-disable-line no-unused-vars import {ConsoleViewport, ConsoleViewportElement, ConsoleViewportProvider} from './ConsoleViewport.js'; // eslint-disable-line no-unused-vars +import {ConsoleDiracPrompt} from './ConsoleDiracPrompt.js'; /** @type {!ConsoleView} */ let consoleViewInstance; @@ -126,6 +128,11 @@ export class ConsoleView extends UI.Widget.VBox { this.setMinimumSize(0, 35); this.registerRequiredCSS('console/consoleView.css'); this.registerRequiredCSS('object_ui/objectValue.css'); + this.registerRequiredCSS('console/dirac-hacks.css'); + this.registerRequiredCSS('console/dirac-codemirror.css'); + this.registerRequiredCSS('console/dirac-theme.css'); + this.registerRequiredCSS('console/dirac-prompt.css'); + dirac.initConsole(); this._searchableView = new UI.SearchableView.SearchableView(this); this._searchableView.element.classList.add('console-searchable-view'); @@ -298,6 +305,13 @@ export class ConsoleView extends UI.Widget.VBox { this._promptElement = this._messagesElement.createChild('div', 'source-code'); this._promptElement.id = 'console-prompt'; + const diracPromptElement = this._messagesElement.createChild('div', 'source-code'); + diracPromptElement.id = 'console-prompt-dirac'; + diracPromptElement.spellcheck = false; + const diracPromptCodeMirrorInstance = dirac.adoptPrompt(diracPromptElement, dirac.hasParinfer); + + diracPromptElement.classList.add('inactive-prompt'); + // FIXME: This is a workaround for the selection machinery bug. See crbug.com/410899 const selectAllFixer = this._messagesElement.createChild('div', 'console-view-fix-select-all'); selectAllFixer.textContent = '.'; @@ -336,13 +350,71 @@ export class ConsoleView extends UI.Widget.VBox { this._updateFilterStatus(); this._timestampsSetting.addChangeListener(this._consoleTimestampsSettingChanged, this); + /** @type {!Object.} */ + this._pendingDiracCommands = {}; + this._lastDiracCommandId = 1; + this._prompts = []; + this._prompts.push({id: 'js', + prompt: this._prompt, + element: this._promptElement, + proxy: this._prompt.element}); + this._activePromptIndex = 0; + + if (dirac.hasREPL) { + const diracPrompt = new ConsoleDiracPrompt(diracPromptCodeMirrorInstance); + diracPrompt.setAutocompletionTimeout(0); + diracPrompt.renderAsBlock(); + const diracProxyElement = diracPrompt.attach(diracPromptElement); + diracProxyElement.classList.add('console-prompt-dirac-wrapper'); + diracProxyElement.addEventListener('keydown', this._promptKeyDown.bind(this), true); + + this._diracHistorySetting = self.Common.settings.createLocalSetting('diracHistory', []); + const diracHistoryData = this._diracHistorySetting.get(); + diracPrompt.history().setHistoryData(diracHistoryData); + + const statusElement = diracPromptElement.createChild('div'); + statusElement.id = 'console-status-dirac'; + + const statusBannerElement = statusElement.createChild('div', 'status-banner'); + statusBannerElement.addEventListener('click', this._diracStatusBannerClick.bind(this), true); + const statusContentElement = statusElement.createChild('div', 'status-content'); + statusContentElement.tabIndex = 0; // focusable for page-up/down + + this._diracPromptDescriptor = {id: 'dirac', + prompt: diracPrompt, + element: diracPromptElement, + proxy: diracProxyElement, + status: statusElement, + statusContent: statusContentElement, + statusBanner: statusBannerElement, + codeMirror: diracPromptCodeMirrorInstance}; + this._prompts.push(this._diracPromptDescriptor); + } + this._registerWithMessageSink(); UI.Context.Context.instance().addFlavorChangeListener( SDK.RuntimeModel.ExecutionContext, this._executionContextChanged, this); + const defaultPromptIndex = dirac.hostedInExtension ? 0 : 1; + this._consolePromptIndexSetting = self.Common.settings.createLocalSetting('consolePromptIndex', defaultPromptIndex); + + this._consoleFeedback = 0; + + if (dirac.hasREPL) { + this.setDiracPromptMode('status'); + } else { + dirac.feedback('!dirac.hasREPL'); + } + dirac.feedback('ConsoleView constructed'); + if (dirac.hasWelcomeMessage) { + this.displayWelcomeMessage(); + } else { + dirac.feedback('!dirac.hasWelcomeMessage'); + } + this._messagesElement.addEventListener( - 'mousedown', /** @param {!Event} event */ + 'mousedown', /** @param {!Event} event */ event => this._updateStickToBottomOnPointerDown(/** @type {!MouseEvent} */ (event).button === 2), false); this._messagesElement.addEventListener('mouseup', this._updateStickToBottomOnPointerUp.bind(this), false); this._messagesElement.addEventListener('mouseleave', this._updateStickToBottomOnPointerUp.bind(this), false); @@ -352,6 +424,8 @@ export class ConsoleView extends UI.Widget.VBox { this._messagesElement.addEventListener('touchend', this._updateStickToBottomOnPointerUp.bind(this), false); this._messagesElement.addEventListener('touchcancel', this._updateStickToBottomOnPointerUp.bind(this), false); + SDK.ConsoleModel.ConsoleModel.instance().addEventListener( + SDK.ConsoleModel.Events.DiracMessage, this._onConsoleDiracMessage, this); SDK.ConsoleModel.ConsoleModel.instance().addEventListener( SDK.ConsoleModel.Events.ConsoleCleared, this._consoleCleared, this); SDK.ConsoleModel.ConsoleModel.instance().addEventListener( @@ -536,6 +610,7 @@ export class ConsoleView extends UI.Widget.VBox { } _executionContextChanged() { + this._switchToLastPrompt(); this._prompt.clearAutocomplete(); } @@ -680,6 +755,553 @@ export class ConsoleView extends UI.Widget.VBox { this._lastShownHiddenByFilterCount = this._hiddenByFilterCount; } + _switchToLastPrompt() { + this._switchPromptIfAvail(this._activePromptIndex, this._consolePromptIndexSetting.get()); + } + + /** + * @param {!Event} event + */ + _diracStatusBannerClick(event) { + if (!event.target || event.target.tagName !== 'A') { + return false; + } + if (this._diracPromptDescriptor.statusBannerCallback) { + this._diracPromptDescriptor.statusBannerCallback('click', event); + } + return false; + } + + setDiracPromptStatusContent(s) { + dirac.feedback("setDiracPromptStatusContent('" + s + "')"); + this._diracPromptDescriptor.statusContent.innerHTML = s; + } + + setDiracPromptStatusBanner(s) { + dirac.feedback("setDiracPromptStatusBanner('" + s + "')"); + this._diracPromptDescriptor.statusBanner.innerHTML = s; + } + + setDiracPromptStatusBannerCallback(callback) { + this._diracPromptDescriptor.statusBannerCallback = callback; + } + + /** + * @param {string} style + */ + setDiracPromptStatusStyle(style) { + dirac.feedback("setDiracPromptStatusStyle('" + style + "')"); + const knownStyles = ['error', 'info']; + if (knownStyles.indexOf(style) === -1) { + console.warn('unknown style passed to setDiracPromptStatusStyle:', style); + } + for (let i = 0; i < knownStyles.length; i++) { + const s = knownStyles[i]; + this._diracPromptDescriptor.status.classList.toggle('dirac-prompt-status-' + s, style === s); + } + } + + /** + * @param {string} mode + */ + setDiracPromptMode(mode) { + dirac.feedback("setDiracPromptMode('" + mode + "')"); + const knownModes = ['edit', 'status']; + if (knownModes.indexOf(mode) === -1) { + console.warn('unknown mode passed to setDiracPromptMode:', mode); + } + for (let i = 0; i < knownModes.length; i++) { + const m = knownModes[i]; + this._diracPromptDescriptor.element.classList.toggle('dirac-prompt-mode-' + m, mode === m); + } + if (mode === 'edit') { + this.focus(); + } + } + + /** + * @param {string} namespace + * @param {string | null} compiler + */ + _buildPromptPlaceholder(namespace, compiler) { + const placeholderEl = document.createElement('div'); + placeholderEl.classList.add('dirac-prompt-placeholder'); + const namespaceEl = document.createElement('span'); + namespaceEl.classList.add('dirac-prompt-namespace'); + namespaceEl.textContent = namespace || ''; + if (compiler) { + const compilerEl = document.createElement('span'); + compilerEl.classList.add('dirac-prompt-compiler'); + compilerEl.textContent = compiler; + placeholderEl.appendChildren(namespaceEl, compilerEl); + } else { + placeholderEl.appendChildren(namespaceEl); + } + return placeholderEl; + } + + _refreshPromptInfo() { + const promptDescriptor = this._prompts[this._activePromptIndex]; + if (promptDescriptor.id !== 'dirac') { + return; + } + + const namespace = this._currentNamespace || ''; + const compiler = this._currentCompiler; + const placeholderEl = this._buildPromptPlaceholder(namespace, compiler); + const cm = promptDescriptor.codeMirror; + // code mirror won't switch the placeholder if the input has focus + const hadFocus = cm.hasFocus(); + if (hadFocus) { + cm.display.input.blur(); + } + promptDescriptor.codeMirror.setOption('placeholder', placeholderEl); + if (hadFocus) { + cm.focus(); + } + } + + /** + * @param {string} name + */ + setDiracPromptNS(name) { + dirac.feedback("setDiracPromptNS('" + name + "')"); + this._currentNamespace = name; + if (this._diracPromptDescriptor) { + this._diracPromptDescriptor.prompt.setCurrentClojureScriptNamespace(name); + } + this._refreshPromptInfo(); + } + + /** + * @param {string} name + */ + setDiracPromptCompiler(name) { + // dirac.feedback("setDiracPromptCompiler('"+name+"')"); + this._currentCompiler = name; + this._refreshPromptInfo(); + } + + /** + * @param {number} _requestId + */ + onJobStarted(_requestId) { + dirac.feedback('repl eval job started'); + } + + /** + * @param {number} requestId + */ + onJobEnded(requestId) { + delete this._pendingDiracCommands[requestId]; + dirac.feedback('repl eval job ended'); + } + + /** + * @return {string} + */ + getSuggestBoxRepresentation() { + const promptDescriptor = this.getCurrentPromptDescriptor(); + return promptDescriptor.id + ' prompt: ' + promptDescriptor.prompt.getSuggestBoxRepresentation(); + } + + /** + * @return {string} + */ + getPromptRepresentation() { + return this._prompt.text(); + } + + /** + * @param {*} message + */ + handleEvalCLJSConsoleDiracMessage(message) { + const code = message.parameters[2]; + if (code && typeof code.value === 'string') { + this.appendDiracCommand(code.value, null); + } + } + + /** + * @param {*} message + */ + handleEvalJSConsoleDiracMessage(message) { + const code = message.parameters[2]; + if (code && typeof code.value === 'string') { + const jsPromptDescriptor = this._getPromptDescriptor('js'); + if (jsPromptDescriptor) { + jsPromptDescriptor.prompt._appendCommand(code.value, true); + } + } + } + + /** + * @param {!Common.EventTarget.EventTargetEvent} event + */ + _onConsoleDiracMessage(event) { + const message = (event.data); + let command = message.parameters[1]; + if (command) { + command = command.value; + } + + switch (command) { + case 'eval-cljs': + this.handleEvalCLJSConsoleDiracMessage(message); + break; + case 'eval-js': + this.handleEvalJSConsoleDiracMessage(message); + break; + default: + throw ('unrecognized Dirac message: ' + command); + } + } + + + /** + * @param {!SDK.ConsoleModel.ConsoleMessage} message + * @return {?string} + */ + _alterDiracViewMessage(message) { + const nestingLevel = this._currentGroup.nestingLevel(); + + message.messageText = ''; + if (message.parameters) { + message.parameters.shift(); // "~~$DIRAC-LOG$~~" + } + + // do not display location link + message.url = undefined; + message.stackTrace = undefined; + + let requestId = -1; + let kind = ''; + try { + if (message.parameters) { + requestId = /** @type {number} */(message.parameters.shift().value); // request-id + kind = /** @type {string} */(message.parameters.shift().value); + } + } catch (e) { + } + + if (kind === 'result') { + message.type = SDK.ConsoleModel.MessageType.Result; + } + + const originatingMessage = this._pendingDiracCommands[requestId]; + if (originatingMessage) { + message.setOriginatingMessage(originatingMessage); + this._pendingDiracCommands[requestId] = message; + } + + return kind ? ('dirac-' + kind) : null; + } + + /** + * @param {?SDK.ConsoleModel.MessageLevel} level + * @returns {string} + */ + _levelForFeedback(level) { + return level || '???'; + } + + /** + * @param {!SDK.ConsoleModel.MessageType} messageType + * @param {boolean} isDiracFlavored + * @returns {string} + */ + _typeForFeedback(messageType, isDiracFlavored) { + if (isDiracFlavored) { + return 'DF'; + } + if (messageType === SDK.ConsoleModel.MessageType.DiracCommand) { + return 'DC'; + } + return 'JS'; + } + + /** + * @param {!SDK.ConsoleModel.ConsoleMessage} message + */ + _createViewMessage(message) { + // this is a HACK to treat REPL messages as Dirac results + const isDiracFlavoredMessage = message.messageText === '~~$DIRAC-LOG$~~'; + let extraClass = null; + + if (isDiracFlavoredMessage) { + extraClass = this._alterDiracViewMessage(message); + } + + const result = this._createViewMessage2(message); + + if (isDiracFlavoredMessage) { + const wrapperElement = result.element(); + wrapperElement.classList.add('dirac-flavor'); + if (extraClass) { + wrapperElement.classList.add(extraClass); + } + } + + if (this._consoleFeedback) { + const levelText = this._levelForFeedback(message.level); + const typeText = this._typeForFeedback(/** @type {!SDK.ConsoleModel.MessageType} */(message.type), isDiracFlavoredMessage); + const contentEl = result.contentElement(); + const consoleMessageTextEl = contentEl.querySelector('.console-message-text'); + if (consoleMessageTextEl) { + const messageText = consoleMessageTextEl.deepTextContent(); + const glue = (messageText.indexOf('\n') === -1) ? '> ' : '>\n'; // log multi-line log messages on a new line + dirac.feedback(typeText + '.' + levelText + glue + messageText); + } + } + + return result; + } + + /** + * @param {string} markup + * @return {boolean} + */ + appendDiracMarkup(markup) { + const target = self.SDK.targetManager.mainTarget(); + if (!target) { + return false; + } + const runtimeModel = target.model(self.SDK.RuntimeModel); + if (!runtimeModel) { + return false; + } + const source = SDK.ConsoleModel.MessageSource.Other; + const level = SDK.ConsoleModel.MessageLevel.Info; + const type = SDK.ConsoleModel.MessageType.DiracMarkup; + const message = new self.SDK.ConsoleMessage(runtimeModel, source, level, markup, type); + self.SDK.consoleModel.addMessage(message); + return true; + } + + displayWelcomeMessage() { + dirac.feedback('displayWelcomeMessage'); + /** + * @param {string} text + */ + const wrapCode = text => { + return "" + text + ''; + }; + /** + * @param {string} text + */ + const wrapBold = text => { + return '' + text + ''; + }; + + const welcomeMessage = + 'Welcome to ' + wrapBold('Dirac DevTools v' + dirac.getVersion()) + '.' + + ' Cycle CLJS/JS prompts with ' + wrapCode('CTRL+,') + '.' + + ' Enter ' + wrapCode('dirac') + ' for additional info.'; + + if (!this.appendDiracMarkup(welcomeMessage)) { + console.warn('displayWelcomeMessage: unable to add console message'); + } + } + + /** + * @param {number} index + */ + _normalizePromptIndex(index) { + const count = this._prompts.length; + while (index < 0) { + index += count; + } + return index % count; + } + + /** + * @param {number} oldPromptIndex + * @param {number} newPromptIndex + */ + _switchPromptIfAvail(oldPromptIndex, newPromptIndex) { + const oldIndex = this._normalizePromptIndex(oldPromptIndex); + const newIndex = this._normalizePromptIndex(newPromptIndex); + if (oldIndex === newIndex) { + return; // nothing to do + } + + this._switchPrompt(oldIndex, newIndex); + } + + /** + * @param {number} oldPromptIndex + * @param {number} newPromptIndex + */ + _switchPrompt(oldPromptIndex, newPromptIndex) { + const oldPromptDescriptor = this._prompts[this._normalizePromptIndex(oldPromptIndex)]; + const newPromptDescriptor = this._prompts[this._normalizePromptIndex(newPromptIndex)]; + + newPromptDescriptor.element.classList.remove('inactive-prompt'); + + this._prompt = newPromptDescriptor.prompt; + this._promptElement = newPromptDescriptor.element; + this._activePromptIndex = this._normalizePromptIndex(newPromptIndex); + this._consolePromptIndexSetting.set(this._activePromptIndex); + this._searchableView.setDefaultFocusedElement(this._promptElement); + + oldPromptDescriptor.element.classList.add('inactive-prompt'); + + dirac.feedback("switched console prompt to '" + newPromptDescriptor.id + "'"); + this._prompt.setText(''); // clear prompt when switching + this.focus(); + + if (newPromptDescriptor.id === 'dirac') { + dirac.initRepl(); + } + } + + _selectNextPrompt() { + this._switchPromptIfAvail(this._activePromptIndex, this._activePromptIndex + 1); + } + + _selectPrevPrompt() { + this._switchPromptIfAvail(this._activePromptIndex, this._activePromptIndex - 1); + } + + /** + * @param {string} promptId + */ + _findPromptIndexById(promptId) { + for (let i = 0; i < this._prompts.length; i++) { + const promptDescriptor = this._prompts[i]; + if (promptDescriptor.id === promptId) { + return i; + } + } + return null; + } + + /** + * @param {string} promptId + */ + _getPromptDescriptor(promptId) { + const promptIndex = this._findPromptIndexById(promptId); + if (promptIndex === null) { + return null; + } + return this._prompts[promptIndex]; + } + + /** + * @param {string} promptId + */ + switchPrompt(promptId) { + const selectedPromptIndex = this._findPromptIndexById(promptId); + if (selectedPromptIndex === null) { + console.warn('switchPrompt: unknown prompt id ', promptId); + return; + } + this._switchPromptIfAvail(this._activePromptIndex, selectedPromptIndex); + } + + /** + * @return {!Object} + */ + getCurrentPromptDescriptor() { + return this._prompts[this._activePromptIndex]; + } + + /** + * @return {!HTMLElement} + */ + getTargetForPromptEvents() { + const promptDescriptor = this.getCurrentPromptDescriptor(); + let inputEl = promptDescriptor.proxy; + if (promptDescriptor.codeMirror) { + inputEl = promptDescriptor.codeMirror.getInputField(); + } + return inputEl; + } + + /** + * @param {string} input + * @return {!Promise} + */ + dispatchEventsForPromptInput(input) { + return new Promise(resolve => { + const continuation = () => resolve("entered input: '" + input + "'"); + const keyboard = Keysim.Keyboard.US_ENGLISH; + keyboard.dispatchEventsForInput(input, this.getTargetForPromptEvents(), continuation); + }); + } + + /** + * @param {string} action + * @return {!Promise} + */ + dispatchEventsForPromptAction(action) { + return new Promise(resolve => { + const continuation = () => resolve("performed action: '" + action + "'"); + const keyboard = Keysim.Keyboard.US_ENGLISH; + keyboard.dispatchEventsForAction(action, this.getTargetForPromptEvents(), continuation); + }); + } + + /** + * @return {number} + */ + enableConsoleFeedback() { + this._consoleFeedback++; + return this._consoleFeedback; + } + + /** + * @return {number} + */ + disableConsoleFeedback() { + this._consoleFeedback--; + return this._consoleFeedback; + } + + /** + * @param {string} text + * @param {?number} id + */ + appendDiracCommand(text, id) { + if (!text) + {return;} + + if (!id) { + id = this._lastDiracCommandId++; + } + + const command = text; + const commandId = id; + + const executionContext = self.UI.context.flavor(self.SDK.ExecutionContext); + if (!executionContext) { + return; + } + + this._prompt.setText(''); + const runtimeModel = executionContext.runtimeModel; + const type = SDK.ConsoleModel.MessageType.DiracCommand; + const source = SDK.ConsoleModel.MessageSource.JS; + const level = SDK.ConsoleModel.MessageLevel.Info; + const commandMessage = new self.SDK.ConsoleMessage(runtimeModel, source, level, text, type); + commandMessage.setExecutionContextId(executionContext.id); + self.SDK.consoleModel.addMessage(commandMessage); + + this._prompt.history().pushHistoryItem(text); + this._diracHistorySetting.set(this._prompt.history().historyData().slice(-persistedHistorySize)); + + const debuggerModel = executionContext.debuggerModel; + let scopeInfoPromise = Promise.resolve(null); + if (debuggerModel) { + scopeInfoPromise = dirac.extractScopeInfoFromScopeChainAsync(debuggerModel.selectedCallFrame()); + } + + this._pendingDiracCommands[commandId] = commandMessage; + scopeInfoPromise.then(function(scopeInfo) { + dirac.sendEvalRequest(commandId, command, scopeInfo); + }); + } + /** * @param {!Common.EventTarget.EventTargetEvent} event */ @@ -688,6 +1310,13 @@ export class ConsoleView extends UI.Widget.VBox { this._addConsoleMessage(message); } + /** + * @param {!SDK.ConsoleModel.ConsoleMessage} message + */ + _normalizeMessageTimestamp(message) { + message.timestamp = this._consoleMessages.length ? this._consoleMessages.peekLast().consoleMessage().timestamp : 0; + } + /** * @param {!SDK.ConsoleModel.ConsoleMessage} message */ @@ -838,11 +1467,15 @@ export class ConsoleView extends UI.Widget.VBox { * @param {!SDK.ConsoleModel.ConsoleMessage} message * @return {!ConsoleViewMessage} */ - _createViewMessage(message) { + _createViewMessage2(message) { const nestingLevel = this._currentGroup.nestingLevel(); switch (message.type) { case SDK.ConsoleModel.MessageType.Command: return new ConsoleCommand(message, this._linkifier, nestingLevel, this._onMessageResizedBound); + case SDK.ConsoleModel.MessageType.DiracCommand: + return new ConsoleDiracCommand(message, this._linkifier, nestingLevel, this._onMessageResizedBound); + case SDK.ConsoleModel.MessageType.DiracMarkup: + return new ConsoleDiracMarkup(message, this._linkifier, nestingLevel, this._onMessageResizedBound); case SDK.ConsoleModel.MessageType.Result: return new ConsoleCommandResult(message, this._linkifier, nestingLevel, this._onMessageResizedBound); case SDK.ConsoleModel.MessageType.StartGroupCollapsed: @@ -1154,6 +1787,18 @@ export class ConsoleView extends UI.Widget.VBox { this._shortcuts.set( UI.KeyboardShortcut.KeyboardShortcut.makeKey('u', UI.KeyboardShortcut.Modifiers.Ctrl), this._clearPromptBackwards.bind(this)); + + const section = self.UI.shortcutsScreen.section(Common.UIString.UIString('Console')); + const shortcut = UI.KeyboardShortcut.KeyboardShortcut; + if (dirac.hasREPL) { + const keys = [ + shortcut.makeDescriptor(UI.KeyboardShortcut.Keys.Comma, UI.KeyboardShortcut.Modifiers.Ctrl), + shortcut.makeDescriptor(UI.KeyboardShortcut.Keys.Period, UI.KeyboardShortcut.Modifiers.Ctrl) + ]; + this._shortcuts[keys[0].key] = this._selectNextPrompt.bind(this); + this._shortcuts[keys[1].key] = this._selectPrevPrompt.bind(this); + section.addRelatedKeys(keys, Common.UIString.UIString('Next/previous prompt')); + } } _clearPromptBackwards() { @@ -1164,10 +1809,30 @@ export class ConsoleView extends UI.Widget.VBox { * @param {!Event} event */ _promptKeyDown(event) { - const keyboardEvent = /** @type {!KeyboardEvent} */ (event); + const keyboardEvent = /** @type {!KeyboardEvent} */(event); if (keyboardEvent.key === 'PageUp') { this._updateStickToBottomOnWheel(); return; + } if (isEnterKey(keyboardEvent)) { + // TODO: this should be eventually moved to ConsoleDiracPrompt.js + // let's wait for upstream to finish transition to ConsolePrompt.js + const promptDescriptor = this._prompts[this._activePromptIndex]; + if (promptDescriptor.id === 'dirac') { + if (event.altKey || event.ctrlKey || event.shiftKey) + {return;} + + event.consume(true); + + this._prompt.clearAutocomplete(); + + const str = this._prompt.text(); + if (!str.length) { + return; + } + + this.appendDiracCommand(str, null); + return; + } } const shortcut = UI.KeyboardShortcut.KeyboardShortcut.makeKeyFromEvent(keyboardEvent); @@ -1209,8 +1874,10 @@ export class ConsoleView extends UI.Widget.VBox { const data = /** @type {{result: ?SDK.RemoteObject.RemoteObject, commandMessage: !SDK.ConsoleModel.ConsoleMessage, exceptionDetails: (!Protocol.Runtime.ExceptionDetails|undefined)}} */ (event.data); - this._prompt.history().pushHistoryItem(data.commandMessage.messageText); - this._consoleHistorySetting.set(this._prompt.history().historyData().slice(-persistedHistorySize)); + if (!data.commandMessage.skipHistory) { + this._prompt.history().pushHistoryItem(data.commandMessage.messageText); + this._consoleHistorySetting.set(this._prompt.history().historyData().slice(-persistedHistorySize)); + } this._printResult(data.result, data.commandMessage, data.exceptionDetails); } @@ -1649,6 +2316,132 @@ export class ConsoleViewFilter { } } +export class ConsoleCommand extends ConsoleViewMessage { + /** + * @param {!SDK.ConsoleModel.ConsoleMessage} consoleMessage + * @param {!Components.Linkifier.Linkifier} linkifier + * @param {number} nestingLevel + * @param {function(!Common.EventTarget.EventTargetEvent):void} onResize + */ + constructor(consoleMessage, linkifier, nestingLevel, onResize) { + super(consoleMessage, linkifier, nestingLevel, onResize); + /** @type {?HTMLElement} */ + this._formattedCommand = null; + } + + /** + * @override + * @return {!HTMLElement} + */ + contentElement() { + const contentElement = this.getContentElement(); + if (contentElement) { + return contentElement; + } + const newContentElement = /** @type {!HTMLElement} */ (document.createElement('div')); + this.setContentElement(newContentElement); + newContentElement.classList.add('console-user-command'); + const icon = UI.Icon.Icon.create('smallicon-user-command', 'command-result-icon'); + newContentElement.appendChild(icon); + + // ts-expect-error We can't convert this to a Weakmap, as it comes from `ConsoleViewMessage` instead. + newContentElement.message = this; + + this._formattedCommand = /** @type {!HTMLElement} */ (document.createElement('span')); + this._formattedCommand.classList.add('source-code'); + this._formattedCommand.textContent = Platform.StringUtilities.replaceControlCharacters(this.text); + newContentElement.appendChild(this._formattedCommand); + + if (this._formattedCommand.textContent.length < MaxLengthToIgnoreHighlighter) { + const javascriptSyntaxHighlighter = new UI.SyntaxHighlighter.SyntaxHighlighter('text/javascript', true); + javascriptSyntaxHighlighter.syntaxHighlightNode(this._formattedCommand).then(this._updateSearch.bind(this)); + } else { + this._updateSearch(); + } + + this.updateTimestamp(); + return newContentElement; + } + + _updateSearch() { + this.setSearchRegex(this.searchRegex()); + } +} + +/** + * @unrestricted + */ +class ConsoleDiracCommand extends ConsoleCommand { + /** + * @override + * @return {!Element} + */ + contentElement() { + if (!this._contentElement) { + this._contentElement = document.createElement('div'); + this._contentElement.classList.add('console-user-command'); + this._contentElement.message = this; + const icon = UI.Icon.Icon.create('smallicon-user-command', 'command-result-icon'); + this._contentElement.appendChild(icon); + + this._formattedCommand = document.createElement('span'); + this._formattedCommand.classList.add('console-message-text', 'source-code', 'cm-s-dirac'); + this._contentElement.appendChild(this._formattedCommand); + + CodeMirror.runMode(this.text, 'clojure-parinfer', this._formattedCommand, undefined); + + this.element().classList.add('dirac-flavor'); // applied to wrapper element + } + return this._contentElement; + } +} + +/** + * @unrestricted + */ +class ConsoleDiracMarkup extends ConsoleCommand { + /** + * @override + * @return {!Element} + */ + contentElement() { + if (!this._contentElement) { + this._contentElement = document.createElement('div'); + this._contentElement.classList.add('console-message', 'console-dirac-markup'); + this._contentElement.message = this; + + this._formattedCommand = document.createElement('span'); + this._formattedCommand.classList.add('console-message-text', 'source-code'); + this._formattedCommand.innerHTML = this.consoleMessage().messageText; + this._contentElement.appendChild(this._formattedCommand); + + this.element().classList.add('dirac-flavor'); // applied to wrapper element + } + return this._contentElement; + } +} + +/** + * @unrestricted + */ +class ConsoleCommandResult extends ConsoleViewMessage { + /** + * @override + * @return {!HTMLElement} + */ + contentElement() { + const element = super.contentElement(); + if (!element.classList.contains('console-user-command-result')) { + element.classList.add('console-user-command-result'); + if (this.consoleMessage().level === SDK.ConsoleModel.MessageLevel.Info) { + const icon = UI.Icon.Icon.create('smallicon-command-result', 'command-result-icon'); + element.insertBefore(icon, element.firstChild); + } + } + return element; + } +} + /** * @unrestricted */ @@ -1732,5 +2525,5 @@ const consoleMessageToViewMessage = new WeakMap(); /** * @typedef {{messageIndex: number, matchIndex: number}} */ -// @ts-expect-error typedef +// ts-expect-error typedef export let RegexMatchRange; diff --git a/front_end/console/ConsoleViewMessage.js b/front_end/console/ConsoleViewMessage.js index d25c6588ae..470ed61627 100644 --- a/front_end/console/ConsoleViewMessage.js +++ b/front_end/console/ConsoleViewMessage.js @@ -881,6 +881,13 @@ export class ConsoleViewMessage { } } + function rawFormatter(obj) { + const rawElement = createElement('div'); + rawElement.setAttribute('class', 'raw-console-output'); + rawElement.innerHTML = obj.description || ''; + return rawElement; + } + /** * @param {string} property */ @@ -916,6 +923,8 @@ export class ConsoleViewMessage { formatters._ = bypassFormatter; + formatters.r = rawFormatter; + /** * @param {!HTMLElement} a * @param {*} b diff --git a/front_end/console/Images b/front_end/console/Images new file mode 120000 index 0000000000..378ca6f03f --- /dev/null +++ b/front_end/console/Images @@ -0,0 +1 @@ +../Images \ No newline at end of file diff --git a/front_end/console/clojure-parinfer.js b/front_end/console/clojure-parinfer.js new file mode 100644 index 0000000000..76be3f457b --- /dev/null +++ b/front_end/console/clojure-parinfer.js @@ -0,0 +1,315 @@ +// Copyright 2020 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. + +/* + * To Parinfer developers, + * + * This is a syntax-highlighting mode for Clojure, copied from CodeMirror. + * We modify it for Parinfer so that it dims the inferred parens at the end of a line. + * (Search for Parinfer below for the relevant edit) + * + * For the purpose of extra-highlighting, we also modify it by tracking a previousToken + * so we can highlight def'd symbols and symbols that are called to. Example: + * + * (def foo 123) (bar 123) + * ^^^ ^^^ + * |------------------|------------- highlighted as 'def' token type + * + * This Clojure mode also has logic for where to indent the cursor when pressing enter. + * We do not modify this. + * + */ + + +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +/** + * Author: Hans Engel + * Branched from CodeMirror's Scheme mode (by Koh Zi Han, based on implementation by Koh Zi Chun) + */ + +(function(mod) { + if (typeof exports === 'object' && typeof module === 'object') // CommonJS + {mod(require('../../lib/codemirror'));} + else if (typeof define === 'function' && define.amd) // AMD + {define(['../../lib/codemirror'], mod);} + else // Plain browser env + {mod(CodeMirror);} +})(function(CodeMirror) { +'use strict'; + +CodeMirror.defineMode('clojure-parinfer', function(options) { + const BUILTIN = 'builtin', COMMENT = 'comment', STRING = 'string', CHARACTER = 'string-2', + NUMBER = 'number', BRACKET = 'bracket', SPECIAL = 'special', VAR = 'variable', KEYWORD = 'keyword', + BOOL = 'bool', NIL = 'nil', + SOL = 'sol', EOL = 'eol', MOL = 'mol', // close-paren styles + DEF = 'def'; + const INDENT_WORD_SKIP = options.indentUnit || 2; + const NORMAL_INDENT_UNIT = options.indentUnit || 2; + + function makeKeywords(str) { + const obj = {}, words = str.split(' '); + for (let i = 0; i < words.length; ++i) {obj[words[i]] = true;} + return obj; + } + + const nils = makeKeywords('nil'); + + const bools = makeKeywords('true false'); + + const defs = makeKeywords( + 'defn defn- def defonce defmulti defmethod defmacro defstruct deftype ns'); + + const specials = makeKeywords( + 'defn defn- def def- defonce defmulti defmethod defmacro defstruct deftype defprotocol defrecord defproject deftest slice defalias defhinted defmacro- defn-memo defnk defnk defonce- defunbound defunbound- defvar defvar- let letfn do case cond condp for loop recur when when-not when-let when-first if if-let if-not . .. -> ->> doto and or dosync doseq dotimes dorun doall load import unimport ns in-ns refer try catch finally throw with-open with-local-vars binding gen-class gen-and-load-class gen-and-save-class handler-case handle'); + + const builtins = makeKeywords( + "* *' *1 *2 *3 *agent* *allow-unresolved-vars* *assert* *clojure-version* *command-line-args* *compile-files* *compile-path* *compiler-options* *data-readers* *e *err* *file* *flush-on-newline* *fn-loader* *in* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *unchecked-math* *use-context-classloader* *verbose-defrecords* *warn-on-reflection* + +' - -' -> ->> ->ArrayChunk ->Vec ->VecNode ->VecSeq -cache-protocol-fn -reset-methods .. / < <= = == > >= EMPTY-NODE accessor aclone add-classpath add-watch agent agent-error agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint biginteger binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* bound? butlast byte byte-array bytes case cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec dec' decimal? declare default-data-readers definline definterface defmacro defmethod defmulti defn defn- defonce defprotocol defrecord defstruct deftype delay delay? deliver denominator deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq error-handler error-mode eval even? every-pred every? ex-data ex-info extend extend-protocol extend-type extenders extends? false? ffirst file-seq filter filterv find find-keyword find-ns find-protocol-impl find-protocol-method find-var first flatten float float-array float? floats flush fn fn? fnext fnil for force format frequencies future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator group-by hash hash-combine hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc inc' init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt keep keep-indexed key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map-indexed map? mapcat mapv max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod munge name namespace namespace-munge neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext nthrest num number? numerator object-array odd? or parents partial partition partition-all partition-by pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-dup print-method print-simple print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int rand-nth range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string realized? reduce reduce-kv reductions ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure reify release-pending-sends rem remove remove-all-methods remove-method remove-ns remove-watch repeat repeatedly replace replicate require reset! reset-meta! resolve rest restart-agent resultset-seq reverse reversible? rseq rsubseq satisfies? second select-keys send send-off seq seq? seque sequence sequential? set set-error-handler! set-error-mode! set-validator! set? short short-array shorts shuffle shutdown-agents slurp some some-fn sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-symbol? spit split-at split-with str string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync take take-last take-nth take-while test the-ns thread-bound? time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-add-int unchecked-byte unchecked-char unchecked-dec unchecked-dec-int unchecked-divide-int unchecked-double unchecked-float unchecked-inc unchecked-inc-int unchecked-int unchecked-long unchecked-multiply unchecked-multiply-int unchecked-negate unchecked-negate-int unchecked-remainder-int unchecked-short unchecked-subtract unchecked-subtract-int underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector-of vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision with-redefs with-redefs-fn xml-seq zero? zipmap *default-data-reader-fn* as-> cond-> cond->> reduced reduced? send-via set-agent-send-executor! set-agent-send-off-executor! some-> some->>"); + + const indentKeys = makeKeywords( + // Built-ins + 'ns fn def defn defmethod bound-fn if if-not case condp when while when-not when-first do future comment doto locking proxy with-open with-precision reify deftype defrecord defprotocol extend extend-protocol extend-type try catch ' + + + // Binding forms + 'let letfn binding loop for doseq dotimes when-let if-let ' + + + // Data structures + 'defstruct struct-map assoc ' + + + // clojure.test + 'testing deftest ' + + + // contrib + 'handler-case handle dotrace deftrace'); + + const tests = { + digit: /\d/, + digit_or_colon: /[\d:]/, + hex: /[0-9a-f]/i, + sign: /[+-]/, + exponent: /e/i, + keyword_char: /[^\s\(\[\;\)\]]/, + symbol: /[\w*+!\-\._?:<>\/\xa1-\uffff]/ + }; + + function stateStack(indent, type, prev) { // represents a state stack object + this.indent = indent; + this.type = type; + this.prev = prev; + } + + function pushStack(state, indent, type) { + state.indentStack = new stateStack(indent, type, state.indentStack); + } + + function popStack(state) { + state.indentStack = state.indentStack.prev; + } + + function isNumber(ch, stream){ + // hex + if (ch === '0' && stream.eat(/x/i)) { + stream.eatWhile(tests.hex); + return true; + } + + // leading sign + if ((ch == '+' || ch == '-') && (tests.digit.test(stream.peek()))) { + stream.eat(tests.sign); + ch = stream.next(); + } + + if (tests.digit.test(ch)) { + stream.eat(ch); + stream.eatWhile(tests.digit); + + if ('.' == stream.peek()) { + stream.eat('.'); + stream.eatWhile(tests.digit); + } + + if (stream.eat(tests.exponent)) { + stream.eat(tests.sign); + stream.eatWhile(tests.digit); + } + + return true; + } + + return false; + } + + // Eat character that starts after backslash \ + function eatCharacter(stream) { + const first = stream.next(); + // Read special literals: backspace, newline, space, return. + // Just read all lowercase letters. + if (first && first.match(/[a-z]/) && stream.match(/[a-z]+/, true)) { + return; + } + // Read unicode character: \u1000 \uA0a1 + if (first === 'u') { + stream.match(/[0-9a-z]{4}/i, true); + } + } + + return { + startState: function() { + return { + previousToken: null, + indentStack: null, + indentation: 0, + mode: false, + atStart: false + }; + }, + + token: function(stream, state) { + + if (stream.sol()) { + state.atStart = (state.mode != 'string'); + } + + if (state.indentStack == null && stream.sol()) { + // update indentation, but only if indentStack is empty + state.indentation = stream.indentation(); + } + + // skip spaces + if (stream.eatSpace()) { + return null; + } + let returnType = null; + let previousToken = null; + + switch (state.mode){ + case 'string': // multi-line string parsing mode + var next, escaped = false; + while ((next = stream.next()) != null) { + if (next == '"' && !escaped) { + + state.mode = false; + break; + } + escaped = !escaped && next == '\\'; + } + returnType = STRING; // continue on in string mode + break; + default: // default parsing mode + var ch = stream.next(); + + if (ch == '"') { + state.mode = 'string'; + returnType = STRING; + } else if (ch == '\\') { + eatCharacter(stream); + returnType = CHARACTER; + } else if (ch == "'" && !(tests.digit_or_colon.test(stream.peek()))) { + returnType = KEYWORD; + } else if (ch == ';') { // comment + stream.skipToEnd(); // rest of the line is a comment + returnType = COMMENT; + } else if (isNumber(ch,stream)){ + returnType = NUMBER; + } else if (ch == '(' || ch == '[' || ch == '{') { + if (ch == '(') { + previousToken = '('; + } + + let keyWord = '', indentTemp = stream.column(), letter; + /** + Either + (indent-word .. + (non-indent-word .. + (;something else, bracket, etc. + */ + + if (ch == '(') {while ((letter = stream.eat(tests.keyword_char)) != null) { + keyWord += letter; + }} + + if (keyWord.length > 0 && (indentKeys.propertyIsEnumerable(keyWord) || + /^(?:def|with)/.test(keyWord))) { // indent-word + pushStack(state, indentTemp + INDENT_WORD_SKIP, ch); + } else { // non-indent word + // we continue eating the spaces + stream.eatSpace(); + if (stream.eol() || stream.peek() == ';') { + // nothing significant after + // we restart indentation the user defined spaces after + pushStack(state, indentTemp + NORMAL_INDENT_UNIT, ch); + } else { + pushStack(state, indentTemp + stream.current().length, ch); // else we match + } + } + stream.backUp(stream.current().length - 1); // undo all the eating + + returnType = BRACKET; + } else if (ch == ')' || ch == ']' || ch == '}') { + returnType = BRACKET; + + // Parinfer: (style trailing delimiters) + stream.eatWhile(/[\s,\]})]/); + if (stream.eol() || stream.peek() == ';') { + returnType += ' ' + EOL; + } else if (state.atStart) { + returnType += ' ' + SOL; + } else { + returnType += ' ' + MOL; + } + stream.backUp(stream.current().length - 1); + + if (state.indentStack != null && state.indentStack.type == (ch == ')' ? '(' : (ch == ']' ? '[' : '{'))) { + popStack(state); + } + } else if (ch == ':') { + stream.eatWhile(tests.symbol); + return KEYWORD; + } else { + stream.eatWhile(tests.symbol); + + if (specials && specials.propertyIsEnumerable(stream.current())) { + returnType = SPECIAL; + } else if (builtins && builtins.propertyIsEnumerable(stream.current())) { + returnType = BUILTIN; + } else if (bools && bools.propertyIsEnumerable(stream.current())) { + returnType = BOOL; + } else if (nils && nils.propertyIsEnumerable(stream.current())) { + returnType = NIL; + } else if (state.previousToken == 'def' || state.previousToken == '(') { + returnType = DEF; + } else { + returnType = VAR; + } + + if (state.previousToken == '(' && defs && defs.propertyIsEnumerable(stream.current())) { + previousToken = 'def'; + } + } + + if (!(ch == ')' || ch == ']' || ch == '}')) { + state.atStart = false; + } + } + + state.previousToken = previousToken; + + return returnType; + }, + + indent: function(state) { + if (state.indentStack == null) {return state.indentation;} + return state.indentStack.indent; + }, + + closeBrackets: {pairs: '()[]{}""'}, + lineComment: ';;' + }; +}); + +CodeMirror.defineMIME('text/x-clojure', 'clojure'); +CodeMirror.registerHelper('wordChars', 'clojure-parinfer', /[^\s()[]{},`']/); + +}); diff --git a/front_end/console/console.js b/front_end/console/console.js index 470b2ba96c..1ed2a0c1e4 100644 --- a/front_end/console/console.js +++ b/front_end/console/console.js @@ -11,6 +11,7 @@ import './ConsoleViewMessage.js'; import './ConsolePrompt.js'; import './ConsoleView.js'; import './ConsolePanel.js'; +import './ConsoleDiracPrompt.js'; import * as ConsoleContextSelector from './ConsoleContextSelector.js'; import * as ConsoleFilter from './ConsoleFilter.js'; @@ -21,6 +22,7 @@ import * as ConsoleSidebar from './ConsoleSidebar.js'; import * as ConsoleView from './ConsoleView.js'; import * as ConsoleViewMessage from './ConsoleViewMessage.js'; import * as ConsoleViewport from './ConsoleViewport.js'; +import * as ConsoleDiracPrompt from './ConsoleDiracPrompt.js'; export { ConsoleContextSelector, @@ -32,4 +34,5 @@ export { ConsoleView, ConsoleViewMessage, ConsoleViewport, + ConsoleDiracPrompt, }; diff --git a/front_end/console/dirac-codemirror.css b/front_end/console/dirac-codemirror.css new file mode 100644 index 0000000000..36e00d1d62 --- /dev/null +++ b/front_end/console/dirac-codemirror.css @@ -0,0 +1,521 @@ +/* BASICS */ + +#console-prompt-dirac .CodeMirror { + /* Set height, width, borders, and global font properties here */ + color: black; + height: inherit; + font-family: Menlo, monospace; + line-height: 1em; + /* darwin: this min-height is important for some reason + in some cases empty dirac prompt wasn't visible because it had 0 height and + it was impossible to activate it by clicking to give it focus + */ + min-height: 1em; +} + +/* PADDING */ + +#console-prompt-dirac .CodeMirror-lines { + padding: 0 0; /* Vertical padding around content */ +} + +#console-prompt-dirac .CodeMirror pre { + padding: 0 0; /* Horizontal padding of content */ +} + +#console-prompt-dirac .CodeMirror-scrollbar-filler, +#console-prompt-dirac .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +#console-prompt-dirac .CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} + +#console-prompt-dirac .CodeMirror-linenumbers { +} + +#console-prompt-dirac .CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +#console-prompt-dirac .CodeMirror-guttermarker { + color: black; +} + +#console-prompt-dirac .CodeMirror-guttermarker-subtle { + color: #999; +} + +/* CURSOR */ + +#console-prompt-dirac .CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} + +/* Shown when moving in bi-directional text */ +#console-prompt-dirac .CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} + +#console-prompt-dirac .cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} + +#console-prompt-dirac .cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +#console-prompt-dirac .cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} + +@-moz-keyframes blink { + 0% { + } + 50% { + background-color: transparent; + } + 100% { + } +} + +@-webkit-keyframes blink { + 0% { + } + 50% { + background-color: transparent; + } + 100% { + } +} + +@keyframes blink { + 0% { + } + 50% { + background-color: transparent; + } + 100% { + } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +#console-prompt-dirac .CodeMirror-overwrite .CodeMirror-cursor { +} + +#console-prompt-dirac .cm-tab { + display: inline-block; + text-decoration: inherit; +} + +#console-prompt-dirac .CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +#console-prompt-dirac .cm-s-default .cm-header { + color: blue; +} + +#console-prompt-dirac .cm-s-default .cm-quote { + color: #090; +} + +#console-prompt-dirac .cm-negative { + color: #d44; +} + +#console-prompt-dirac .cm-positive { + color: #292; +} + +#console-prompt-dirac .cm-header, .cm-strong { + font-weight: bold; +} + +#console-prompt-dirac .cm-em { + font-style: italic; +} + +#console-prompt-dirac .cm-link { + text-decoration: underline; +} + +#console-prompt-dirac .cm-strikethrough { + text-decoration: line-through; +} + +#console-prompt-dirac .cm-s-default .cm-keyword { + color: #708; +} + +#console-prompt-dirac .cm-s-default .cm-atom { + color: #219; +} + +#console-prompt-dirac .cm-s-default .cm-number { + color: #164; +} + +#console-prompt-dirac .cm-s-default .cm-def { + color: #00f; +} + +#console-prompt-dirac .cm-s-default .cm-variable, +#console-prompt-dirac .cm-s-default .cm-punctuation, +#console-prompt-dirac .cm-s-default .cm-property, +#console-prompt-dirac .cm-s-default .cm-operator { +} + +#console-prompt-dirac .cm-s-default .cm-variable-2 { + color: #05a; +} + +#console-prompt-dirac .cm-s-default .cm-variable-3 { + color: #085; +} + +#console-prompt-dirac .cm-s-default .cm-comment { + color: #a50; +} + +#console-prompt-dirac .cm-s-default .cm-string { + color: #a11; +} + +#console-prompt-dirac .cm-s-default .cm-string-2 { + color: #f50; +} + +#console-prompt-dirac .cm-s-default .cm-meta { + color: #555; +} + +#console-prompt-dirac .cm-s-default .cm-qualifier { + color: #555; +} + +#console-prompt-dirac .cm-s-default .cm-builtin { + color: #30a; +} + +#console-prompt-dirac .cm-s-default .cm-bracket { + color: #997; +} + +#console-prompt-dirac .cm-s-default .cm-tag { + color: #170; +} + +#console-prompt-dirac .cm-s-default .cm-attribute { + color: #00c; +} + +#console-prompt-dirac .cm-s-default .cm-hr { + color: #999; +} + +#console-prompt-dirac .cm-s-default .cm-link { + color: #00c; +} + +#console-prompt-dirac .cm-s-default .cm-error { + color: #f00; +} + +#console-prompt-dirac .cm-invalidchar { + color: #f00; +} + +#console-prompt-dirac .CodeMirror-composing { + border-bottom: 2px solid; +} + +/* Default styles for common addons */ + +/* Parinfer edit: don't change the colors, we just want to style the background +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +*/ +#console-prompt-dirac .CodeMirror-matchingtag { + background: rgba(255, 150, 0, .3); +} + +#console-prompt-dirac .CodeMirror-activeline-background { + background: #e8f2ff; +} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +#console-prompt-dirac .CodeMirror { + position: relative; + overflow: hidden; + /* background: white; */ +} + +#console-prompt-dirac .CodeMirror-scroll { + /* darwin: I had to force these styles to make codemirror auto-resize working + see http://web.mit.edu/xavid/Public/bazki/lib/ext/codemirror/demo/resize.html + */ + outline: none !important; /* Prevent dragging from highlighting the element */ + position: initial !important; + height: auto !important; + overflow-y: hidden !important; + overflow-x: auto !important; +} + +#console-prompt-dirac .CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +#console-prompt-dirac .CodeMirror-vscrollbar, +#console-prompt-dirac .CodeMirror-hscrollbar, +#console-prompt-dirac .CodeMirror-scrollbar-filler, +#console-prompt-dirac .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} + +#console-prompt-dirac .CodeMirror-vscrollbar { + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; +} + +#console-prompt-dirac .CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; +} + +#console-prompt-dirac .CodeMirror-scrollbar-filler { + right: 0; + bottom: 0; +} + +#console-prompt-dirac .CodeMirror-gutter-filler { + left: 0; + bottom: 0; +} + +#console-prompt-dirac .CodeMirror-gutters { + position: absolute; + left: 0; + top: 0; + z-index: 3; +} + +#console-prompt-dirac .CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + margin-bottom: -30px; + /* Hack to make IE7 behave */ + *zoom: 1; + *display: inline; +} + +#console-prompt-dirac .CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} + +#console-prompt-dirac .CodeMirror-gutter-background { + position: absolute; + top: 0; + bottom: 0; + z-index: 4; +} + +#console-prompt-dirac .CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} + +#console-prompt-dirac .CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +#console-prompt-dirac .CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} + +#console-prompt-dirac .CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} + +#console-prompt-dirac .CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +#console-prompt-dirac .CodeMirror-linebackground { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 0; +} + +#console-prompt-dirac .CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +#console-prompt-dirac .CodeMirror-widget { +} + +#console-prompt-dirac .CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +#console-prompt-dirac .CodeMirror-scroll, +#console-prompt-dirac .CodeMirror-sizer, +#console-prompt-dirac .CodeMirror-gutter, +#console-prompt-dirac .CodeMirror-gutters, +#console-prompt-dirac .CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +#console-prompt-dirac .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +#console-prompt-dirac .CodeMirror-cursor { + position: absolute; +} + +#console-prompt-dirac .CodeMirror-measure pre { + position: static; +} + +#console-prompt-dirac div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} + +#console-prompt-dirac div.CodeMirror-dragcursors { + visibility: visible; +} + +#console-prompt-dirac .CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +#console-prompt-dirac .CodeMirror-selected { + background: #d9d9d9; +} + +#console-prompt-dirac .CodeMirror-focused .CodeMirror-selected { + background: #d7d4f0; +} + +#console-prompt-dirac .CodeMirror-crosshair { + cursor: crosshair; +} + +#console-prompt-dirac .CodeMirror-line::selection, +#console-prompt-dirac .CodeMirror-line > span::selection, +#console-prompt-dirac .CodeMirror-line > span > span::selection { + background: #d7d4f0; +} + +#console-prompt-dirac .CodeMirror-line::-moz-selection, +#console-prompt-dirac .CodeMirror-line > span::-moz-selection, +#console-prompt-dirac .CodeMirror-line > span > span::-moz-selection { + background: #d7d4f0; +} + +#console-prompt-dirac .cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +#console-prompt-dirac .CodeMirror span { + *vertical-align: text-bottom; +} + +/* Used to force a border model for a node */ +/*#console-prompt-dirac .cm-force-border {*/ + /*padding-right: .1px;*/ +/*}*/ + +/* See issue #2901 */ +#console-prompt-dirac .cm-tab-wrap-hack:after { + content: ''; +} + +/* Help users use markselection to safely style text background */ +#console-prompt-dirac span.CodeMirror-selectedtext { + background: none; +} + +/* HACK - hscrollbar started causing troubles, not sure why + Dirac's codemirror component should never end-up showing scrollbars anyways +*/ +#console-prompt-dirac .CodeMirror-vscrollbar, +#console-prompt-dirac .CodeMirror-hscrollbar { + display: none !important; +} diff --git a/front_end/console/dirac-hacks.css b/front_end/console/dirac-hacks.css new file mode 100644 index 0000000000..bd38b310a6 --- /dev/null +++ b/front_end/console/dirac-hacks.css @@ -0,0 +1,23 @@ +/* let's try to keep our css overrides here to avoid conflicts upstream */ + +.console-message-text > * { + z-index: 10; + position: relative; +} + +.console-message-url { + position: absolute; + right: 0; + z-index: 0; + opacity: 0.2; +} + +.console-message-wrapper.console-adjacent-user-command-result { + border-bottom: 1px solid transparent; +} + +.console-message-wrapper.console-warning-level, +.console-message-wrapper.console-error-level { + border-top: none; + margin: 0; +} \ No newline at end of file diff --git a/front_end/console/dirac-prompt.css b/front_end/console/dirac-prompt.css new file mode 100644 index 0000000000..1376d6b2ab --- /dev/null +++ b/front_end/console/dirac-prompt.css @@ -0,0 +1,131 @@ +.console-prompt-dirac-wrapper { + background-color: rgba(100, 255, 100, 0.08); +} + +.dirac-flavor { + background-color: rgba(100, 255, 100, 0.08); +} + +.dirac-flavor::before { + background-position: -20px -20px; + -webkit-filter: hue-rotate(280deg); +} + +.console-dirac-markup { + line-height: 14px; +} + +.console-message, .console-user-command { + padding-left: 4px !important; + padding-right: 4px !important; +} + +.dirac-stderr > .console-message { + background-color: rgba(255, 0, 0, 0.15) !important; + margin-right: 24px; +} + +.dirac-stdout > .console-message { + background-color: rgba(0, 0, 0, 0.05) !important; + margin-right: 24px; +} + +.console-error-level { + background-color: hsl(0, 100%, 97%); +} + +.console-warning-level { + background-color: hsl(50, 100%, 95%); +} + +#console-prompt { + padding-left: 4px !important; + padding-right: 4px !important; +} + +#console-prompt-dirac { + clear: right; + position: relative; + padding: 3px 22px 1px 0; + padding-left: 4px !important; + padding-right: 4px !important; + margin-left: 24px; + min-height: 18px; /* Sync with ConsoleViewMessage.js */ + width: 100%; +} + +#console-prompt-dirac::before { + background-position: -80px 90px; + -webkit-filter: hue-rotate(280deg); +} + +.inactive-prompt { + display: none; +} + +.dirac-prompt-mode-status #cm-console-prompt-dirac { + display: none; +} + +.dirac-prompt-mode-edit #console-status-dirac { + display: none; +} + +#console-prompt-dirac::before { + position: absolute; + display: block; + content: ""; + left: -17px; + top: 9px; + width: 10px; + height: 10px; + margin-top: -4px; + -webkit-user-select: none; + background-image: url(Images/smallIcons.svg); +} + +#console-status-dirac .status-banner { + float: right; + color: #aaa; +} + +#console-status-dirac .status-banner a { + color: #aaa; + text-decoration: underline; +} + +#console-status-dirac .status-banner a:hover { + color: #999; + cursor: pointer !important; +} + +#console-status-dirac .status-content { + font-style: italic; +} + +#console-status-dirac.dirac-prompt-status-info .status-content, +#console-status-dirac.dirac-prompt-status-info .status-content a { + color: #00f; +} + +#console-status-dirac.dirac-prompt-status-error .status-content, +#console-status-dirac.dirac-prompt-status-error .status-content a { + color: #f00; +} + +.dirac-prompt-placeholder { +} + +.dirac-prompt-namespace { + color: rgba(102, 200, 102, 0.6); +} + +.dirac-prompt-compiler { + color: rgba(190, 109, 199, 0.6); +} + +.dirac-prompt-compiler::before { + content: "\0000BB"; + padding: 0px 10px; + color: rgba(153, 153, 153, 0.3); +} diff --git a/front_end/console/dirac-theme.css b/front_end/console/dirac-theme.css new file mode 100644 index 0000000000..dea1c5a0de --- /dev/null +++ b/front_end/console/dirac-theme.css @@ -0,0 +1,88 @@ +/* THEME MATCHING CLJS-DEVTOOLS FORMATTER */ + +.cm-s-dirac .cm-special { + font-weight: bold; + color: #000000; +} + +/* clojure core forms */ +.cm-s-dirac .cm-builtin { + color: #000000; +} + +/* clojure core library */ +.cm-s-dirac .cm-string { + color: #C41A16; +} + +/* clojure strings */ +.cm-s-dirac .cm-variable { + color: #333333; +} + +/* clojure misc symbols */ +.cm-s-dirac .cm-def { + color: #000000; +} + +/* clojure function calls or defs */ +.cm-s-dirac .cm-keyword { + color: #881391; +} + +/* clojure keyword */ +.cm-s-dirac .cm-bool { + color: #009999; +} + +/* clojure boolean */ +.cm-s-dirac .cm-nil { + color: #808080; +} + +/* clojure nil */ +.cm-s-dirac .cm-number { + color: #1C00CF; +} + +/* clojure number */ +.cm-s-dirac .cm-bracket { + color: #333333; +} + +/* clojure brackets */ +.cm-s-dirac .cm-comment { + color: #aaaaaa; +} + +/* clojure comment */ + +.CodeMirror pre { + line-height: 14px; +} + +div.CodeMirror span.CodeMirror-matchingbracket { + background: #FFFF90; + color: inherit; + border-bottom: 1px solid #111; +} + +div.CodeMirror span.CodeMirror-nonmatchingbracket { +} + +/* don't change selection color based on an editor's focus status. */ +.CodeMirror-selected { + background: #d7d4f0; +} + +.CodeMirror-focused .CodeMirror-selected { + background: #d7d4f0; +} + +.CodeMirror.cm-x-parinfer .cm-bracket.cm-eol { + opacity: 0.4; +} + +.CodeMirror.cm-x-parinfer .parinfer-error { + background: #FFBEBE; +} diff --git a/front_end/console/module.json b/front_end/console/module.json index 5dad545821..619948efd7 100644 --- a/front_end/console/module.json +++ b/front_end/console/module.json @@ -212,6 +212,8 @@ ], "dependencies": [ "components", + "source_frame", + "dirac", "data_grid", "host", "object_ui", @@ -219,6 +221,9 @@ "formatter", "browser_sdk" ], + "scripts": [ + "clojure-parinfer.js" + ], "modules": [ "console.js", "console-legacy.js", @@ -230,13 +235,21 @@ "ConsoleViewMessage.js", "ConsolePrompt.js", "ConsoleView.js", - "ConsolePanel.js" + "ConsolePanel.js", + "ConsoleDiracPrompt.js" + ], + "skip_compilation": [ + "clojure-parinfer.js" ], "resources": [ "consoleContextSelector.css", "consolePinPane.css", "consolePrompt.css", "consoleSidebar.css", - "consoleView.css" + "consoleView.css", + "dirac-hacks.css", + "dirac-codemirror.css", + "dirac-theme.css", + "dirac-prompt.css" ] -} \ No newline at end of file +} diff --git a/front_end/dirac/dirac.js b/front_end/dirac/dirac.js new file mode 100644 index 0000000000..88b7f6079b --- /dev/null +++ b/front_end/dirac/dirac.js @@ -0,0 +1,333 @@ +// @ts-nocheck +// Copyright 2020 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 './keysim.js'; +import './parinfer.js'; +import './parinfer-codemirror.js'; + +console.log('dirac module import!'); + +(function () { + const window = this; + + // dirac namespace may not exist at this point, play safe + if (!window.dirac) { + window.dirac = {}; + } + + // note: if goog/cljs namespace system comes after us, they don't wipe our properties, they just merge theirs in + Object.assign(window.dirac, (function () { + const readyPromise = new Promise(fulfil => window.dirac._runtimeReadyPromiseCallback = fulfil); + + function getReadyPromise() { + return readyPromise; + } + + function markAsReady() { + window.dirac._runtimeReadyPromiseCallback(); + } + + const featureFlags = {}; + + // WARNING: keep this in sync with dirac.background.tools/flag-keys + const knownFeatureFlags = [ + 'enable-repl', + 'enable-parinfer', + 'enable-friendly-locals', + 'enable-clustered-locals', + 'inline-custom-formatters', + 'welcome-message', + 'clean-urls', + 'beautify-function-names', + 'link-actions']; + + function hasFeature(feature) { + const flag = featureFlags[feature]; + if (flag !== undefined) { + return flag; + } + const featureIndex = knownFeatureFlags.indexOf(feature); + if (featureIndex === -1) { + return true; + } + const activeFlags = Root.Runtime.queryParam('dirac_flags') || ''; + const result = activeFlags[featureIndex] !== '0'; + featureFlags[feature] = result; + return result; + } + + function getToggle(name) { + if (window.dirac.DEBUG_TOGGLES) { + console.log("dirac: get toggle '" + name + "' => " + window.dirac[name]); + } + return window.dirac[name]; + } + + function setToggle(name, value) { + if (window.dirac.DEBUG_TOGGLES) { + console.log("dirac: set toggle '" + name + "' => " + value); + } + window.dirac[name] = value; + } + + function hasDebugFlag(flagName) { + if (Root.Runtime.queryParam('debug_all') === '1') { + return true; + } + const paramName = 'debug_' + flagName.toLowerCase(); + return Root.Runtime.queryParam(paramName) === '1'; + } + + // taken from https://github.com/joliss/js-string-escape/blob/master/index.js + function stringEscape(string) { + return ('' + string).replace(/["'\\\n\r\u2028\u2029]/g, function (character) { + // Escape all characters not included in SingleStringCharacters and + // DoubleStringCharacters on + // http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4 + switch (character) { + case '"': + case "'": + case '\\': + return '\\' + character; + // Four possible LineTerminator characters need to be escaped: + case '\n': + return '\\n'; + case '\r': + return '\\r'; + case '\u2028': + return '\\u2028'; + case '\u2029': + return '\\u2029'; + } + }); + } + + function codeAsString(code) { + return "'" + stringEscape(code) + "'"; + } + + function loadLazyDirac() { + return window.runtime.loadModulePromise('dirac_lazy'); + } + + function deduplicate(coll, keyFn = item => '' + item) { + const store = new Set(); + return coll.filter(item => !store.has(keyFn(item)) && !!store.add(keyFn(item))); + } + + // http://stackoverflow.com/a/20767836/84283 + function stableSort(array, comparator) { + const wrapped = array.map((d, i) => ({d: d, i: i})); + + wrapped.sort((a, b) => { + const cmp = comparator(a.d, b.d); + return cmp === 0 ? a.i - b.i : cmp; + }); + + return wrapped.map(wrapper => wrapper.d); + } + + function getNamespace(namespaceName) { + if (!dirac.namespacesCache) { + return; + } + + return dirac.namespacesCache[namespaceName]; + } + + function dispatchEventsForAction(action) { + return new Promise(resolve => { + const continuation = () => resolve("performed document action: '" + action + "'"); + const keyboard = Keysim.Keyboard.US_ENGLISH; + keyboard.dispatchEventsForAction(action, window['document'], continuation); + }); + } + + /** + * @suppressGlobalPropertiesCheck + **/ + function collectShadowRoots(root = null) { + const res = []; + const startNode = root || document.body; + for (let node = startNode; node; node = node.traverseNextNode(startNode)) { + if (node instanceof ShadowRoot) { + res.push(node); + } + } + return res; + } + + function querySelectorAllDeep(node, query) { + const roots = [node].concat(collectShadowRoots(node)); + let res = []; + for (const node of roots) { + const partial = node.querySelectorAll(query); + res = res.concat(Array.from(partial)); + } + return res; + } + + // --- lazy APIs -------------------------------------------------------------------------------------------------------- + // calling any of these functions will trigger loading dirac_lazy overlay + // which will eventually overwrite those functions when fully loaded + + function startListeningForWorkspaceChanges(...args) { + return loadLazyDirac().then(() => window.dirac.startListeningForWorkspaceChanges(...args)); + } + + function stopListeningForWorkspaceChanges(...args) { + return loadLazyDirac().then(() => window.dirac.stopListeningForWorkspaceChanges(...args)); + } + + function extractScopeInfoFromScopeChainAsync(...args) { + return loadLazyDirac().then(() => window.dirac.extractScopeInfoFromScopeChainAsync(...args)); + } + + function extractNamespaceSymbolsAsync(...args) { + return loadLazyDirac().then(() => window.dirac.extractNamespaceSymbolsAsync(...args)); + } + + function invalidateNamespaceSymbolsCache(...args) { + return loadLazyDirac().then(() => window.dirac.invalidateNamespaceSymbolsCache(...args)); + } + + function extractMacroNamespaceSymbolsAsync(...args) { + return loadLazyDirac().then(() => window.dirac.extractMacroNamespaceSymbolsAsync(...args)); + } + + function extractNamespacesAsync() { + return loadLazyDirac().then(() => window.dirac.extractNamespacesAsync()); + } + + function invalidateNamespacesCache(...args) { + return loadLazyDirac().then(() => window.dirac.invalidateNamespacesCache(...args)); + } + + function getMacroNamespaceNames(...args) { + return loadLazyDirac().then(() => window.dirac.getMacroNamespaceNames(...args)); + } + + function lookupCurrentContext(...args) { + return loadLazyDirac().then(() => window.dirac.lookupCurrentContext(...args)); + } + + function evalInCurrentContext(...args) { + return loadLazyDirac().then(() => window.dirac.evalInCurrentContext(...args)); + } + + function hasCurrentContext() { + return loadLazyDirac().then(() => window.dirac.hasCurrentContext()); + } + + function evalInDefaultContext(...args) { + return loadLazyDirac().then(() => window.dirac.evalInDefaultContext(...args)); + } + + function hasDefaultContext() { + return loadLazyDirac().then(() => window.dirac.hasDefaultContext()); + } + + function getMainDebuggerModel(...args) { + return loadLazyDirac().then(() => window.dirac.getMainDebuggerModel(...args)); + } + + function subscribeDebuggerEvents(...args) { + return loadLazyDirac().then(() => window.dirac.subscribeDebuggerEvents(...args)); + } + + function unsubscribeDebuggerEvents(...args) { + return loadLazyDirac().then(() => window.dirac.unsubscribeDebuggerEvents(...args)); + } + + function addConsoleMessageToMainTarget(...args) { + return loadLazyDirac().then(() => window.dirac.addConsoleMessageToMainTarget(...args)); + } + + function evaluateCommandInConsole(...args) { + return loadLazyDirac().then(() => window.dirac.evaluateCommandInConsole(...args)); + } + + function registerDiracLinkAction(...args) { + return loadLazyDirac().then(() => window.dirac.registerDiracLinkAction(...args)); + } + +// --- exported interface --------------------------------------------------------------------------------------------------- + + // don't forget to update externs.js too + return { + DEBUG_EVAL: hasDebugFlag('eval'), + DEBUG_COMPLETIONS: hasDebugFlag('completions'), + DEBUG_KEYSIM: hasDebugFlag('keysim'), + DEBUG_FEEDBACK: hasDebugFlag('feedback'), + DEBUG_WATCHING: hasDebugFlag('watching'), + DEBUG_CACHES: hasDebugFlag('caches'), + DEBUG_TOGGLES: hasDebugFlag('toggles'), + + // we use can_dock url param indicator if we are launched as internal devtools + hostedInExtension: !Root.Runtime.queryParam('can_dock'), + + // -- feature toggles ----------------------------------------------------------------------------------------------- + hasREPL: hasFeature('enable-repl'), + hasParinfer: hasFeature('enable-parinfer'), + hasFriendlyLocals: hasFeature('enable-friendly-locals'), + hasClusteredLocals: hasFeature('enable-clustered-locals'), + hasInlineCFs: hasFeature('inline-custom-formatters'), + hasWelcomeMessage: hasFeature('welcome-message'), + hasCleanUrls: hasFeature('clean-urls'), + hasBeautifyFunctionNames: hasFeature('beautify-function-names'), + hasLinkActions: hasFeature('link-actions'), + + // -- INTERFACE ----------------------------------------------------------------------------------------------------- + getReadyPromise: getReadyPromise, + markAsReady: markAsReady, + hasFeature: hasFeature, + codeAsString: codeAsString, + stringEscape: stringEscape, + deduplicate: deduplicate, + stableSort: stableSort, + getNamespace: getNamespace, + dispatchEventsForAction: dispatchEventsForAction, + querySelectorAllDeep: querySelectorAllDeep, + setToggle: setToggle, + getToggle: getToggle, + + // -- LAZY INTERFACE ------------------------------------------------------------------------------------------------ + lookupCurrentContext: lookupCurrentContext, + evalInCurrentContext: evalInCurrentContext, + hasCurrentContext: hasCurrentContext, + evalInDefaultContext: evalInDefaultContext, + hasDefaultContext: hasDefaultContext, + getMainDebuggerModel: getMainDebuggerModel, + subscribeDebuggerEvents: subscribeDebuggerEvents, + unsubscribeDebuggerEvents: unsubscribeDebuggerEvents, + addConsoleMessageToMainTarget: addConsoleMessageToMainTarget, + evaluateCommandInConsole: evaluateCommandInConsole, + startListeningForWorkspaceChanges: startListeningForWorkspaceChanges, + stopListeningForWorkspaceChanges: stopListeningForWorkspaceChanges, + extractScopeInfoFromScopeChainAsync: extractScopeInfoFromScopeChainAsync, + extractNamespaceSymbolsAsync: extractNamespaceSymbolsAsync, + extractMacroNamespaceSymbolsAsync: extractMacroNamespaceSymbolsAsync, + extractNamespacesAsync: extractNamespacesAsync, + invalidateNamespaceSymbolsCache: invalidateNamespaceSymbolsCache, + invalidateNamespacesCache: invalidateNamespacesCache, + getMacroNamespaceNames: getMacroNamespaceNames, + registerDiracLinkAction: registerDiracLinkAction, + + // ... + + // note: there will be more functions added to this object dynamically by dirac.implant init code + // see externs.js for full list of avail functions + }; + + })()); + + if (window.dirac.implant) { + window.dirac.implant.init_implant(); + } else { + window.initDiracImplantAfterLoad = true; + } +}).call(self); + +console.log('dirac module imported!'); diff --git a/front_end/dirac/keysim.js b/front_end/dirac/keysim.js new file mode 100644 index 0000000000..75450af52b --- /dev/null +++ b/front_end/dirac/keysim.js @@ -0,0 +1,803 @@ +// @ts-nocheck +// Copyright 2020 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. + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + factory((global.Keysim = {})); +})(self, function (exports) { + 'use strict'; + + const _createClass = (function () { + function defineProperties(target, props) { + for (let i = 0; i < props.length; i++) { + const descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ('value' in descriptor) { + descriptor.writable = true; + } + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) { + defineProperties(Constructor.prototype, protoProps); + } + if (staticProps) { + defineProperties(Constructor, staticProps); + } + return Constructor; + }; + })(); + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError('Cannot call a class as a function'); + } + } + + /** + * @param {string} s + * @return {function(string):string} + */ + const appender = function (s) { + return function (value) { + return value + s; + }; + }; + + /** + * @param {number} n + * @return {function(string):string} + */ + const deleter = function (n) { + return function (value) { + const end = value.length - n; + return (end > 0) ? value.substring(0, end) : ''; + }; + }; + + let taskQueueRunning = false; + const taskQueue = []; + + function processTask(task) { + try { + task.job(); + } catch (e) { + console.error('keysim task has failed:', e, '\n', task); + } + wakeTaskQueue(); + } + + function wakeTaskQueue() { + const task = taskQueue.shift(); + if (task) { + taskQueueRunning = true; + setTimeout(function () { + processTask(task); + }, task.delay); + } else { + taskQueueRunning = false; + } + } + + function scheduleTask(delay, job) { + taskQueue.push({delay, job, stack: new Error('scheduled at')}); + if (!taskQueueRunning) { + wakeTaskQueue(); + } + } + + /* jshint esnext:true, undef:true, unused:true */ + + // taken from devtools.js + // noinspection DuplicatedCode + const staticKeyIdentifiers = new Map([ + [0x12, 'Alt'], + [0x11, 'Control'], + [0x10, 'Shift'], + [0x14, 'CapsLock'], + [0x5b, 'Win'], + [0x5c, 'Win'], + [0x0c, 'Clear'], + [0x28, 'Down'], + [0x23, 'End'], + [0x0a, 'Enter'], + [0x0d, 'Enter'], + [0x2b, 'Execute'], + [0x70, 'F1'], + [0x71, 'F2'], + [0x72, 'F3'], + [0x73, 'F4'], + [0x74, 'F5'], + [0x75, 'F6'], + [0x76, 'F7'], + [0x77, 'F8'], + [0x78, 'F9'], + [0x79, 'F10'], + [0x7a, 'F11'], + [0x7b, 'F12'], + [0x7c, 'F13'], + [0x7d, 'F14'], + [0x7e, 'F15'], + [0x7f, 'F16'], + [0x80, 'F17'], + [0x81, 'F18'], + [0x82, 'F19'], + [0x83, 'F20'], + [0x84, 'F21'], + [0x85, 'F22'], + [0x86, 'F23'], + [0x87, 'F24'], + [0x2f, 'Help'], + [0x24, 'Home'], + [0x2d, 'Insert'], + [0x25, 'Left'], + [0x22, 'PageDown'], + [0x21, 'PageUp'], + [0x13, 'Pause'], + [0x2c, 'PrintScreen'], + [0x27, 'Right'], + [0x91, 'Scroll'], + [0x29, 'Select'], + [0x26, 'Up'], + [0x2e, 'U+007F'], // Standard says that DEL becomes U+007F. + [0xb0, 'MediaNextTrack'], + [0xb1, 'MediaPreviousTrack'], + [0xb2, 'MediaStop'], + [0xb3, 'MediaPlayPause'], + [0xad, 'VolumeMute'], + [0xae, 'VolumeDown'], + [0xaf, 'VolumeUp'], + ]); + + function keyCodeToKeyIdentifier(keyCode) { + let result = staticKeyIdentifiers.get(keyCode); + if (result !== undefined) { + return result; + } + result = 'U+'; + const hexString = Number(keyCode).toString(16).toUpperCase(); + for (let i = hexString.length; i < 4; ++i) { + result += '0'; + } + result += hexString; + return result; + } + + const keyCodeToKeyMap = { + 9: 'Tab', // tab + 16: 'Shift', + 27: 'Escape', // esc + 32: ' ', // space + 38: 'ArrowUp', + 40: 'ArrowDown', + 37: 'ArrowLeft', + 39: 'ArrowRight', + 13: 'Enter', + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', + 46: 'U+007F', + 36: 'Home', + 35: 'End', + 33: 'PageUp', + 34: 'PageDown', + 45: 'Insert' + }; + + function keyCodeToKey(keyCode) { + return keyCodeToKeyMap[keyCode] || String.fromCharCode(keyCode); + } + + + const CTRL = 1 << 0; + const META = 1 << 1; + const ALT = 1 << 2; + const SHIFT = 1 << 3; + + // Key Events + const KeyEvents = { + DOWN: 1 << 0, + PRESS: 1 << 1, + UP: 1 << 2, + INPUT: 1 << 3 + }; + KeyEvents.ALL = KeyEvents.DOWN | KeyEvents.PRESS | KeyEvents.UP | KeyEvents.INPUT; + + /** + * Represents a keystroke, or a single key code with a set of active modifiers. + * @constructor + * @param {number} modifiers A bitmask formed by CTRL, META, ALT, and SHIFT. + * @param {number} keyCode + * @param {?function(string):string} mutation + */ + const Keystroke = (function () { + /** @this {Keystroke} */ + function Keystroke(modifiers, keyCode, mutation = null) { + _classCallCheck(this, Keystroke); + + this.modifiers = modifiers; + this.ctrlKey = !!(modifiers & CTRL); + this.metaKey = !!(modifiers & META); + this.altKey = !!(modifiers & ALT); + this.shiftKey = !!(modifiers & SHIFT); + this.keyCode = keyCode; + this.mutation = mutation; + } + + /** + * Simulates a keyboard with a particular key-to-character and key-to-action + * mapping. Use `US_ENGLISH` to get a pre-configured keyboard. + */ + + /** + * Gets the bitmask value for the "control" modifier. + */ + _createClass(Keystroke, null, [{ + key: 'CTRL', + value: CTRL, + enumerable: true + }, { + key: 'META', + value: META, + enumerable: true + }, { + key: 'ALT', + value: ALT, + enumerable: true + }, { + key: 'SHIFT', + value: SHIFT, + enumerable: true + }]); + + return Keystroke; + })(); + + const Keyboard = (function () { + /** + * @constructor + * @param {!Object.} charCodeKeyCodeMap + * @param {!Object.} actionMap + */ + function Keyboard(charCodeKeyCodeMap, actionMap) { + _classCallCheck(this, Keyboard); + + this._charCodeKeyCodeMap = charCodeKeyCodeMap; + this._actionMap = actionMap; + } + + /** + * Determines the character code generated by pressing the given keystroke. + * + * @param {!Keystroke} keystroke + * @return {?number} + * @this {Keyboard} + */ + Keyboard.prototype.charCodeForKeystroke = function charCodeForKeystroke(keystroke) { + const map = this._charCodeKeyCodeMap; + for (const charCode in map) { + if (Object.prototype.hasOwnProperty.call(map, charCode)) { + const keystrokeForCharCode = map[charCode]; + if (keystroke.keyCode === keystrokeForCharCode.keyCode && keystroke.modifiers === keystrokeForCharCode.modifiers) { + return parseInt(charCode, 10); + } + } + } + return null; + }; + + /** + * Creates an event ready for dispatching onto the given target. + * + * @param {string} type One of "keydown", "keypress", "keyup", or "input". + * @param {!Keystroke} keystroke + * @param {!HTMLElement} target + * @return {!Event} + * @this {Keyboard} + */ + Keyboard.prototype.createEventFromKeystroke = function createEventFromKeystroke(type, keystroke, target) { + let doc = target.ownerDocument || document; + if (target instanceof Document) { + doc = target; + } + + const window = doc.defaultView; + const Event = window.Event; + + let event; + + try { + event = new Event(type); + } catch (e) { + event = doc.createEvent('UIEvents'); + } + + event.initEvent(type, true, true); + + switch (type) { + case 'input': + event.data = String.fromCharCode(this.charCodeForKeystroke(keystroke)); + break; + + case 'keydown': + case 'keypress': + case 'keyup': + event.shiftKey = keystroke.shiftKey; + event.altKey = keystroke.altKey; + event.metaKey = keystroke.metaKey; + event.ctrlKey = keystroke.ctrlKey; + event.keyCode = type === 'keypress' ? this.charCodeForKeystroke(keystroke) : keystroke.keyCode; + event.charCode = type === 'keypress' ? event.keyCode : 0; + event.which = event.keyCode; + event.keyIdentifier = keyCodeToKeyIdentifier(keystroke.keyCode); + event.key = keyCodeToKey(event.keyCode); + break; + } + + return event; + }; + + /** + * Fires the correct sequence of events on the given target as if the given + * action was undertaken by a human. + * + * @param {string} action e.g. "alt+shift+left" or "backspace" + * @param {!HTMLElement} target + * @param {?function()} callback + * @this {Keyboard} + */ + Keyboard.prototype.dispatchEventsForAction = function (action, target, callback) { + const keystroke = this.keystrokeForAction(action); + scheduleTask(50, () => this.dispatchEventsForKeystroke(keystroke, target)); + if (callback) { + scheduleTask(100, callback); + } + }; + + /** + * Fires the correct sequence of events on the given target as if the given + * input had been typed by a human. + * + * @param {string} input + * @param {!HTMLElement} target + * @param {?function(string):string} callback + * @this {Keyboard} + */ + Keyboard.prototype.dispatchEventsForInput = function (input, target, callback) { + let currentModifierState = 0; + for (let i = 0, _length = input.length; i < _length; i++) { + const keystroke = this.keystrokeForCharCode(input.charCodeAt(i)); + scheduleTask(30, ((currentModifierState, keystrokeModifiers) => + this.dispatchModifierStateTransition(target, currentModifierState, keystrokeModifiers)) + .bind(this, currentModifierState, keystroke.modifiers)); + scheduleTask(20, ((keystroke, char) => + this.dispatchEventsForKeystroke(keystroke, target, false, KeyEvents.ALL, appender(char))) + .bind(this, keystroke, input[i])); + currentModifierState = keystroke.modifiers; + } + scheduleTask(20, () => this.dispatchModifierStateTransition(target, currentModifierState, 0)); + if (callback) { + scheduleTask(100, callback); + } + }; + + /** + * Fires the correct sequence of events on the given target as if the given + * keystroke was performed by a human. When simulating, for example, typing + * the letter "A" (assuming a U.S. English keyboard) then the sequence will + * look like this: + * + * keydown keyCode=16 (SHIFT) charCode=0 shiftKey=true + * keydown keyCode=65 (A) charCode=0 shiftKey=true + * keypress keyCode=65 (A) charCode=65 (A) shiftKey=true + * input data=A + * keyup keyCode=65 (A) charCode=0 shiftKey=true + * keyup keyCode=16 (SHIFT) charCode=0 shiftKey=false + * + * If the keystroke would not cause a character to be input, such as when + * pressing alt+shift+left, the sequence looks like this: + * + * keydown keyCode=16 (SHIFT) charCode=0 altKey=false shiftKey=true + * keydown keyCode=18 (ALT) charCode=0 altKey=true shiftKey=true + * keydown keyCode=37 (LEFT) charCode=0 altKey=true shiftKey=true + * keyup keyCode=37 (LEFT) charCode=0 altKey=true shiftKey=true + * keyup keyCode=18 (ALT) charCode=0 altKey=false shiftKey=true + * keyup keyCode=16 (SHIFT) charCode=0 altKey=false shiftKey=false + * + * To disable handling of modifier keys, call with `transitionModifiers` set + * to false. Doing so will omit the keydown and keyup events associated with + * shift, ctrl, alt, and meta keys surrounding the actual keystroke. + * + * @param {!Keystroke} keystroke + * @param {!HTMLElement} target + * @param {boolean=} transitionModifiers + * @param {number} events + * @param {?function(string):string} mutation + * @this {Keyboard} + */ + + Keyboard.prototype.dispatchEventsForKeystroke = function dispatchEventsForKeystroke(keystroke, target, transitionModifiers = true, events = KeyEvents.ALL, mutation = null) { + if (transitionModifiers) { + this.dispatchModifierStateTransition(target, 0, keystroke.modifiers, events); + } + + const dispatchEvent = function (e) { + if (dirac.DEBUG_KEYSIM) { + console.log('event dispatch', e.keyCode, e.type, e); + } + const res = target.dispatchEvent(e); + if (dirac.DEBUG_KEYSIM) { + console.log(' => (event dispatch) ', res); + } + return res; + }; + + let keydownEvent = undefined; + if (events & KeyEvents.DOWN) { + keydownEvent = this.createEventFromKeystroke('keydown', keystroke, target); + } + + if (keydownEvent && dispatchEvent(keydownEvent) && this.targetCanReceiveTextInput(target)) { + let keypressEvent = undefined; + if (events & KeyEvents.PRESS) { + keypressEvent = this.createEventFromKeystroke('keypress', keystroke, target); + } + if (keypressEvent && (keypressEvent.charCode || mutation || keystroke.mutation) && dispatchEvent(keypressEvent)) { + if (events & KeyEvents.INPUT) { + const inputEvent = this.createEventFromKeystroke('input', keystroke, target); + // CodeMirror does read input content back, so we have to add real content into target element + // we currently only support cursor at the end of input, no selection changes, etc. + const effectiveMutation = mutation || keystroke.mutation; + if (effectiveMutation) { + const newValue = effectiveMutation(target.value); + if (dirac.DEBUG_KEYSIM) { + console.log('mutation of value', target.value, newValue, target); + } + target.value = newValue; + } + dispatchEvent(inputEvent); + } + } + } + + if (events & KeyEvents.UP) { + const keyupEvent = this.createEventFromKeystroke('keyup', keystroke, target); + dispatchEvent(keyupEvent); + } + + if (transitionModifiers) { + this.dispatchModifierStateTransition(target, keystroke.modifiers, 0); + } + }; + + /** + * Transitions from one modifier state to another by dispatching key events. + * + * @param {!EventTarget} target + * @param {number} fromModifierState + * @param {number} toModifierState + * @param {number} events + * @this {Keyboard} + * @private + */ + + Keyboard.prototype.dispatchModifierStateTransition = function dispatchModifierStateTransition(target, fromModifierState, toModifierState) { + const events = arguments.length <= 3 || arguments[3] === undefined ? KeyEvents.ALL : arguments[3]; + + let currentModifierState = fromModifierState; + const didHaveMeta = (fromModifierState & META) === META; + const willHaveMeta = (toModifierState & META) === META; + const didHaveCtrl = (fromModifierState & CTRL) === CTRL; + const willHaveCtrl = (toModifierState & CTRL) === CTRL; + const didHaveShift = (fromModifierState & SHIFT) === SHIFT; + const willHaveShift = (toModifierState & SHIFT) === SHIFT; + const didHaveAlt = (fromModifierState & ALT) === ALT; + const willHaveAlt = (toModifierState & ALT) === ALT; + + const includeKeyUp = events & KeyEvents.UP; + const includeKeyPress = events & KeyEvents.PRESS; + const includeKeyDown = events & KeyEvents.DOWN; + + const dispatchEvent = function (e) { + // console.log("dispatch", e); + return target.dispatchEvent(e); + }; + + if (includeKeyUp && didHaveMeta === true && willHaveMeta === false) { + // Release the meta key. + currentModifierState &= ~META; + dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionMap.META.keyCode), target)); + } + + if (includeKeyUp && didHaveCtrl === true && willHaveCtrl === false) { + // Release the ctrl key. + currentModifierState &= ~CTRL; + dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionMap.CTRL.keyCode), target)); + } + + if (includeKeyUp && didHaveShift === true && willHaveShift === false) { + // Release the shift key. + currentModifierState &= ~SHIFT; + dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionMap.SHIFT.keyCode), target)); + } + + if (includeKeyUp && didHaveAlt === true && willHaveAlt === false) { + // Release the alt key. + currentModifierState &= ~ALT; + dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionMap.ALT.keyCode), target)); + } + + if (includeKeyDown && didHaveMeta === false && willHaveMeta === true) { + // Press the meta key. + currentModifierState |= META; + dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionMap.META.keyCode), target)); + } + + if (includeKeyDown && didHaveCtrl === false && willHaveCtrl === true) { + // Press the ctrl key. + currentModifierState |= CTRL; + dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionMap.CTRL.keyCode), target)); + } + + if (includeKeyDown && didHaveShift === false && willHaveShift === true) { + // Press the shift key. + currentModifierState |= SHIFT; + dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionMap.SHIFT.keyCode), target)); + } + + if (includeKeyDown && didHaveAlt === false && willHaveAlt === true) { + // Press the alt key. + currentModifierState |= ALT; + dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionMap.ALT.keyCode), target)); + } + + if (currentModifierState !== toModifierState) { + throw new Error('internal error, expected modifier state: ' + toModifierState + (', got: ' + currentModifierState)); + } + }; + + /** + * Returns the keystroke associated with the given action. + * + * @param {string} action + * @return {?Keystroke} + * @this {Keyboard} + */ + + Keyboard.prototype.keystrokeForAction = function keystrokeForAction(action) { + let keyCode = null; + let modifiers = 0; + let mutation = null; + + const parts = action.split('+'); + const lastPart = parts.pop(); + + parts.forEach(function (part) { + switch (part.toUpperCase()) { + case 'CTRL': + modifiers |= CTRL; + break; + case 'META': + modifiers |= META; + break; + case 'ALT': + modifiers |= ALT; + break; + case 'SHIFT': + modifiers |= SHIFT; + break; + default: + throw new Error('in "' + action + '", invalid modifier: ' + part); + } + }); + + const actionLookup = this._actionMap[lastPart.toUpperCase()]; + if (actionLookup) { + keyCode = actionLookup.keyCode; + mutation = actionLookup.mutation; + } else if (lastPart.length === 1) { + const lastPartKeystroke = this.keystrokeForCharCode(lastPart.charCodeAt(0)); + modifiers |= lastPartKeystroke.modifiers; + keyCode = lastPartKeystroke.keyCode; + } else { + throw new Error('in "' + action + '", invalid action: ' + lastPart); + } + + return new Keystroke(modifiers, keyCode, mutation); + }; + + /** + * Gets the keystroke used to generate the given character code. + * + * @param {number} charCode + * @return {?Keystroke} + * @this {Keyboard} + */ + Keyboard.prototype.keystrokeForCharCode = function keystrokeForCharCode(charCode) { + return this._charCodeKeyCodeMap[charCode]; + }; + + /** + * @param {!EventTarget} target + * @private + */ + Keyboard.prototype.targetCanReceiveTextInput = function targetCanReceiveTextInput(target) { + if (!target) { + return false; + } + + switch (target.nodeName && target.nodeName.toLowerCase()) { + case 'input': + const type = target.type; + return !(type === 'hidden' || type === 'radio' || type === 'checkbox'); + + case 'textarea': + return true; + + default: + return false; + } + }; + + return Keyboard; + })(); + + const US_ENGLISH_CHARCODE_KEYCODE_MAP = { + 32: new Keystroke(0, 32), // + 33: new Keystroke(SHIFT, 49), // ! + 34: new Keystroke(SHIFT, 222), // " + 35: new Keystroke(SHIFT, 51), // # + 36: new Keystroke(SHIFT, 52), // $ + 37: new Keystroke(SHIFT, 53), // % + 38: new Keystroke(SHIFT, 55), // & + 39: new Keystroke(0, 222), // ' + 40: new Keystroke(SHIFT, 57), // ( + 41: new Keystroke(SHIFT, 48), // ) + 42: new Keystroke(SHIFT, 56), // * + 43: new Keystroke(SHIFT, 187), // + + 44: new Keystroke(0, 188), // , + 45: new Keystroke(0, 189), // - + 46: new Keystroke(0, 190), // . + 47: new Keystroke(0, 191), // / + 48: new Keystroke(0, 48), // 0 + 49: new Keystroke(0, 49), // 1 + 50: new Keystroke(0, 50), // 2 + 51: new Keystroke(0, 51), // 3 + 52: new Keystroke(0, 52), // 4 + 53: new Keystroke(0, 53), // 5 + 54: new Keystroke(0, 54), // 6 + 55: new Keystroke(0, 55), // 7 + 56: new Keystroke(0, 56), // 8 + 57: new Keystroke(0, 57), // 9 + 58: new Keystroke(SHIFT, 186), // : + 59: new Keystroke(0, 186), // ; + 60: new Keystroke(SHIFT, 188), // < + 61: new Keystroke(0, 187), // = + 62: new Keystroke(SHIFT, 190), // > + 63: new Keystroke(SHIFT, 191), // ? + 64: new Keystroke(SHIFT, 50), // @ + 65: new Keystroke(SHIFT, 65), // A + 66: new Keystroke(SHIFT, 66), // B + 67: new Keystroke(SHIFT, 67), // C + 68: new Keystroke(SHIFT, 68), // D + 69: new Keystroke(SHIFT, 69), // E + 70: new Keystroke(SHIFT, 70), // F + 71: new Keystroke(SHIFT, 71), // G + 72: new Keystroke(SHIFT, 72), // H + 73: new Keystroke(SHIFT, 73), // I + 74: new Keystroke(SHIFT, 74), // J + 75: new Keystroke(SHIFT, 75), // K + 76: new Keystroke(SHIFT, 76), // L + 77: new Keystroke(SHIFT, 77), // M + 78: new Keystroke(SHIFT, 78), // N + 79: new Keystroke(SHIFT, 79), // O + 80: new Keystroke(SHIFT, 80), // P + 81: new Keystroke(SHIFT, 81), // Q + 82: new Keystroke(SHIFT, 82), // R + 83: new Keystroke(SHIFT, 83), // S + 84: new Keystroke(SHIFT, 84), // T + 85: new Keystroke(SHIFT, 85), // U + 86: new Keystroke(SHIFT, 86), // V + 87: new Keystroke(SHIFT, 87), // W + 88: new Keystroke(SHIFT, 88), // X + 89: new Keystroke(SHIFT, 89), // Y + 90: new Keystroke(SHIFT, 90), // Z + 91: new Keystroke(0, 219), // [ + 92: new Keystroke(0, 220), // \ + 93: new Keystroke(0, 221), // ] + 96: new Keystroke(0, 192), // ` + 97: new Keystroke(0, 65), // a + 98: new Keystroke(0, 66), // b + 99: new Keystroke(0, 67), // c + 100: new Keystroke(0, 68), // d + 101: new Keystroke(0, 69), // e + 102: new Keystroke(0, 70), // f + 103: new Keystroke(0, 71), // g + 104: new Keystroke(0, 72), // h + 105: new Keystroke(0, 73), // i + 106: new Keystroke(0, 74), // j + 107: new Keystroke(0, 75), // k + 108: new Keystroke(0, 76), // l + 109: new Keystroke(0, 77), // m + 110: new Keystroke(0, 78), // n + 111: new Keystroke(0, 79), // o + 112: new Keystroke(0, 80), // p + 113: new Keystroke(0, 81), // q + 114: new Keystroke(0, 82), // r + 115: new Keystroke(0, 83), // s + 116: new Keystroke(0, 84), // t + 117: new Keystroke(0, 85), // u + 118: new Keystroke(0, 86), // v + 119: new Keystroke(0, 87), // w + 120: new Keystroke(0, 88), // x + 121: new Keystroke(0, 89), // y + 122: new Keystroke(0, 90), // z + 123: new Keystroke(SHIFT, 219), // { + 124: new Keystroke(SHIFT, 220), // | + 125: new Keystroke(SHIFT, 221), // } + 126: new Keystroke(SHIFT, 192) // ~ + }; + + const US_ENGLISH_ACTION_MAP = { + BACKSPACE: {keyCode: 8, mutation: deleter(1)}, + TAB: {keyCode: 9, mutation: appender('\t')}, + ENTER: {keyCode: 13, mutation: appender('\n')}, + SHIFT: {keyCode: 16}, + CTRL: {keyCode: 17}, + ALT: {keyCode: 18}, + PAUSE: {keyCode: 19}, + CAPSLOCK: {keyCode: 20}, + ESCAPE: {keyCode: 27}, + SPACE: {keyCode: 32, mutation: appender(' ')}, + PAGEUP: {keyCode: 33}, + PAGEDOWN: {keyCode: 34}, + END: {keyCode: 35}, + HOME: {keyCode: 36}, + LEFT: {keyCode: 37}, + UP: {keyCode: 38}, + RIGHT: {keyCode: 39}, + DOWN: {keyCode: 40}, + INSERT: {keyCode: 45}, + DELETE: {keyCode: 46}, + META: {keyCode: 91}, + F1: {keyCode: 112}, + F2: {keyCode: 113}, + F3: {keyCode: 114}, + F4: {keyCode: 115}, + F5: {keyCode: 116}, + F6: {keyCode: 117}, + F7: {keyCode: 118}, + F8: {keyCode: 119}, + F9: {keyCode: 120}, + F10: {keyCode: 121}, + F11: {keyCode: 122}, + F12: {keyCode: 123} + }; + + /** + * Gets a keyboard instance configured as a U.S. English keyboard would be. + * + * @return {!Keyboard} + */ + Keyboard.US_ENGLISH = new Keyboard(US_ENGLISH_CHARCODE_KEYCODE_MAP, US_ENGLISH_ACTION_MAP); + + exports.KeyEvents = KeyEvents; + exports.Keystroke = Keystroke; + exports.Keyboard = Keyboard; + +}); diff --git a/front_end/dirac/module.json b/front_end/dirac/module.json new file mode 100644 index 0000000000..1a8689ab29 --- /dev/null +++ b/front_end/dirac/module.json @@ -0,0 +1,21 @@ +{ + "dependencies": [ + "platform", + "common", + "host" + ], + "modules": [ + "dirac.js", + "parinfer.js", + "parinfer-codemirror.js", + "keysim.js" + ], + "scripts": [ + "require-implant.js" + ], + "skip_compilation": [ + "require-implant.js" + ], + "resources": [ + ] +} diff --git a/front_end/dirac/parinfer-codemirror.js b/front_end/dirac/parinfer-codemirror.js new file mode 100644 index 0000000000..bb046bfac8 --- /dev/null +++ b/front_end/dirac/parinfer-codemirror.js @@ -0,0 +1,685 @@ +// @ts-nocheck +// Copyright 2020 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. + +// +// Parinfer for CodeMirror 1.4.2 +// +// Copyright 2017 © Shaun Lebron +// MIT License +// + +// ------------------------------------------------------------------------------ +// JS Module Boilerplate +// ('parinfer' is a dependency handled differently depending on environment) +// ------------------------------------------------------------------------------ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define(['parinfer'], factory); + } + // else if (typeof module === 'object' && module.exports) { + // module.exports = factory(require('parinfer')); + // } + else { + root.parinferCodeMirror = factory(root.parinfer); + } +})(self, function (parinfer) { // start module anonymous scope + 'use strict'; + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +// We attach our Parinfer state to this property on the CodeMirror instance. + const STATE_PROP = '__parinfer__'; + + const PAREN_MODE = 'paren'; + const INDENT_MODE = 'indent'; + const SMART_MODE = 'smart'; + + const MODES = [PAREN_MODE, INDENT_MODE, SMART_MODE]; + + const CLASSNAME_ERROR = 'parinfer-error'; + const CLASSNAME_PARENTRAIL = 'parinfer-paren-trail'; + const CLASSNAME_LOCUS_PAREN = 'parinfer-locus-paren'; + + const CLASSNAME_LOCUS_LAYER = 'parinfer-locus'; + +// ------------------------------------------------------------------------------ +// State +// (`state` represents the parinfer state attached to a single CodeMirror editor) +// ------------------------------------------------------------------------------ + + function initialState(cm, mode, options) { + return { + cm: cm, + mode: mode, + options: options, + enabled: false, + cursorTimeout: null, + monitorCursor: true, + prevCursorX: null, + prevCursorLine: null, + callbackCursor: null, + callbackChanges: null, + }; + } + +// ------------------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------------------ + + function error(msg) { + return 'parinferCodeMirror: ' + msg; + } + + function ensureMode(mode) { + if (MODES.indexOf(mode) === -1) { + throw error( + 'Mode "' + mode + '" is invalid. ' + + 'Must be one of: ' + MODES.join(',') + ); + } + } + + function ensureState(cm) { + const state = cm[STATE_PROP]; + if (!state) { + throw error( + 'You must call parinferCodeMirror.init(cm) on a CodeMirror instance ' + + 'before you can use the rest of the API.' + ); + } + return state; + } + +// ------------------------------------------------------------------------------ +// Data conversion +// ------------------------------------------------------------------------------ + + function convertChanges(changes) { + return changes.map(function (change) { + return { + x: change.from.ch, + lineNo: change.from.line, + oldText: change.removed.join('\n'), + newText: change.text.join('\n') + }; + }); + } + +// ------------------------------------------------------------------------------ +// Markers +// ------------------------------------------------------------------------------ + + function clearMarks(cm, className) { + let i; + const marks = cm.getAllMarks(); + for (i = 0; i < marks.length; i++) { + if (marks[i].className === className) { + marks[i].clear(); + } + } + } + + function clearAllMarks(cm) { + clearMarks(cm, CLASSNAME_ERROR); + clearMarks(cm, CLASSNAME_PARENTRAIL); + } + + function addMark(cm, lineNo, x0, x1, className) { + const from = {line: lineNo, ch: x0}; + const to = {line: lineNo, ch: x1}; + cm.markText(from, to, {className: className}); + } + + function updateErrorMarks(cm, error) { + clearMarks(cm, CLASSNAME_ERROR); + if (error) { + addMark(cm, error.lineNo, error.x, error.x + 1, CLASSNAME_ERROR); + if (error.extra) { + addMark(cm, error.extra.lineNo, error.extra.x, error.extra.x + 1, CLASSNAME_ERROR); + } + } + } + + function updateParenTrailMarks(cm, parenTrails) { + clearMarks(cm, CLASSNAME_PARENTRAIL); + if (parenTrails) { + let i, trail; + for (i = 0; i < parenTrails.length; i++) { + trail = parenTrails[i]; + addMark(cm, trail.lineNo, trail.startX, trail.endX, CLASSNAME_PARENTRAIL); + } + } + } + +// ------------------------------------------------------------------------------ +// Tab Stops +// ------------------------------------------------------------------------------ + + function getSelectionStartLine(cm) { + const selection = cm.listSelections()[0]; + // head and anchor are reversed sometimes + return Math.min(selection.head.line, selection.anchor.line); + } + + function expandTabStops(tabStops) { + if (!tabStops) { + return null; + } + const xs = []; + let i, stop, prevX = -1; + for (i = 0; i < tabStops.length; i++) { + stop = tabStops[i]; + if (prevX >= stop.x) { + xs.pop(); + } + xs.push(stop.x); + xs.push(stop.x + (stop.ch === '(' ? 2 : 1)); + if (stop.argX != null) { + xs.push(stop.argX); + } + } + return xs; + } + + function nextStop(stops, x, dx) { + if (!stops) { + return null; + } + let i, stop, right, left; + for (i = 0; i < stops.length; i++) { + stop = stops[i]; + if (x < stop) { + right = stop; + break; + } + if (x > stop) { + left = stop; + } + } + if (dx === -1) { + return left; + } + if (dx === 1) { + return right; + } + } + + function getIndent(cm, lineNo) { + const line = cm.getLine(lineNo); + let i; + for (i = 0; i < line.length; i++) { + if (line[i] !== ' ') { + return i; + } + } + return null; + } + + function indentSelection(cm, dx, stops) { + // Indent whole Selection + const lineNo = getSelectionStartLine(cm); + const x = getIndent(cm, lineNo); + let nextX = nextStop(stops, x, dx); + if (nextX == null) { + nextX = Math.max(0, x + dx * 2); + } + cm.indentSelection(nextX - x); + } + + function indentLine(cm, lineNo, delta) { + const text = cm.getDoc().getLine(lineNo); + + // cm.indentLine does not indent empty lines + if (text.trim() !== '') { + cm.indentLine(lineNo, delta); + return; + } + + if (delta > 0) { + const spaces = Array(delta + 1).join(' '); + cm.replaceSelection(spaces); + } else { + const x = cm.getCursor().ch; + cm.replaceRange('', {line: lineNo, ch: x + delta}, {line: lineNo, ch: x}, '+indent'); + } + } + + function indentAtCursor(cm, dx, stops) { + // Indent single line at cursor + const cursor = cm.getCursor(); + const lineNo = cursor.line; + const x = cursor.ch; + const indent = getIndent(cm, cursor.line); + + const stop = nextStop(stops, x, dx); + const useStops = (indent == null || x === indent); + const nextX = (stop != null && useStops) ? stop : Math.max(0, x + dx * 2); + + if (indent != null && indent < x && x < nextX) { + const spaces = Array(nextX - x + 1).join(' '); + cm.replaceSelection(spaces); + } else { + indentLine(cm, lineNo, nextX - x); + } + } + + function onTab(cm, dx) { + const hasSelection = cm.somethingSelected(); + const state = ensureState(cm); + const stops = expandTabStops(state.tabStops); + + if (hasSelection) { + indentSelection(cm, dx, stops); + } else { + indentAtCursor(cm, dx, stops); + } + } + +// ------------------------------------------------------------------------------ +// Locus/Guides layer +// ------------------------------------------------------------------------------ + + function getLayerContainer(cm) { + const wrapper = cm.getWrapperElement(); + const lines = wrapper.querySelector('.CodeMirror-lines'); + const container = lines.parentNode; + return container; + } + + function parenSelected(paren, sel) { + return sel.contains({line: paren.lineNo, ch: paren.x}) !== -1; + } + + function pointRevealsParenTrail(trail, pos) { + return ( + pos.line === trail.lineNo && + trail.startX <= pos.ch /* && cursor.ch <= trail.endX */ + ); + } + + function hideParen(cm, paren) { + const sel = cm.getDoc().sel; + const sel0 = sel.ranges[0]; + const shouldShowCloser = ( + paren.lineNo === paren.closer.lineNo || + !paren.closer.trail || + pointRevealsParenTrail(paren.closer.trail, sel0.anchor) || + pointRevealsParenTrail(paren.closer.trail, sel0.head) || + parenSelected(paren.closer, sel) + ); + + if (!shouldShowCloser) { + addMark(cm, paren.closer.lineNo, paren.closer.x, paren.closer.x + 1, CLASSNAME_LOCUS_PAREN); + } + hideParens(cm, paren.children); + } + + function hideParens(cm, parens) { + let i; + for (i = 0; i < parens.length; i++) { + hideParen(cm, parens[i]); + } + } + + function charPos(cm, paren) { + const p = cm.charCoords({line: paren.lineNo, ch: paren.x}, 'local'); + const w = p.right - p.left; + return { + midx: p.left + w / 2, + right: p.right, + left: p.left, + top: p.top, + bottom: p.bottom, + }; + } + + function getRightBound(cm, startLine, endLine) { + const doc = cm.getDoc(); + let maxWidth = 0; + let maxLineNo = 0; + let i; + for (i = startLine; i <= endLine; i++) { + const line = doc.getLine(i); + if (line.length > maxWidth) { + maxWidth = line.length; + maxLineNo = i; + } + } + const wall = charPos(cm, {lineNo: maxLineNo, x: maxWidth}); + return wall.right; + } + + function addBox(cm, paren) { + const layer = cm[STATE_PROP].layer; + const paper = layer.paper; + const charW = layer.charW; + const charH = layer.charH; + + const open = charPos(cm, paren); + const close = charPos(cm, paren.closer); + + const r = 4; + + if (paren.closer.trail && paren.lineNo !== paren.closer.lineNo) { + switch (layer.type) { + case 'guides': + paper.path([ + 'M', open.midx, open.bottom, + 'V', close.bottom + ].join(' ')); + break; + case 'locus': + var right = getRightBound(cm, paren.lineNo, paren.closer.lineNo); + paper.path([ + 'M', open.midx, open.top + r, + 'A', r, r, 0, 0, 1, open.midx + r, open.top, + 'H', right - r, + 'A', r, r, 0, 0, 1, right, open.top + r, + 'V', close.bottom, + 'H', open.midx, + 'V', open.bottom + ].join(' ')); + break; + } + } + + addBoxes(cm, paren.children); + } + + function addBoxes(cm, parens) { + let i; + for (i = 0; i < parens.length; i++) { + addBox(cm, parens[i]); + } + } + + function addLayer(cm, type) { + const layer = cm[STATE_PROP].layer; + layer.type = type; + + const el = document.createElement('div'); + el.style.position = 'absolute'; + el.style.left = '0'; + el.style.top = '0'; + el.style['z-index'] = 100; + el.className = CLASSNAME_LOCUS_LAYER; + + layer.el = el; + layer.container.appendChild(el); + + const pixelW = layer.container.clientWidth; + const pixelH = layer.container.clientHeight; + + // layer.paper = Raphael(el, pixelW, pixelH); + } + + function clearLayer(cm) { + const layer = cm[STATE_PROP].layer; + if (layer && layer.el) { + layer.container.removeChild(layer.el); + } + } + + function updateLocusLayer(cm, parens) { + clearMarks(cm, CLASSNAME_LOCUS_PAREN); + if (parens) { + hideParens(cm, parens); + clearLayer(cm); + // addLayer(cm, 'locus'); // don't draw boxes, just draw guides + addLayer(cm, 'guides'); + addBoxes(cm, parens); + } + } + + function updateGuidesLayer(cm, parens) { + if (parens) { + clearLayer(cm); + addLayer(cm, 'guides'); + addBoxes(cm, parens); + } + } + +// ------------------------------------------------------------------------------ +// Text Correction +// ------------------------------------------------------------------------------ + +// If `changes` is missing, then only the cursor position has changed. + function fixText(state, changes) { + // Get editor data + const cm = state.cm; + const text = cm.getValue(); + const hasSelection = cm.somethingSelected(); + const selections = cm.listSelections(); + const cursor = cm.getCursor(); + const scroller = cm.getScrollerElement(); + + // Create options + const options = { + cursorLine: cursor.line, + cursorX: cursor.ch, + prevCursorLine: state.prevCursorLine, + prevCursorX: state.prevCursorX + }; + if (hasSelection) { + options.selectionStartLine = getSelectionStartLine(cm); + } + if (state.options) { + let p; + for (p in state.options) { + if (state.options.hasOwnProperty(p)) { + options[p] = state.options[p]; + } + } + } + if (changes) { + options.changes = convertChanges(changes); + } + + const locus = state.options && state.options.locus; + const guides = state.options && state.options.guides; + + if (locus || guides) { + delete options.locus; + delete options.guides; + options.returnParens = true; + } + + // Run Parinfer + let result; + const mode = state.fixMode ? PAREN_MODE : state.mode; + switch (mode) { + case INDENT_MODE: + result = parinfer.indentMode(text, options); + break; + case PAREN_MODE: + result = parinfer.parenMode(text, options); + break; + case SMART_MODE: + result = parinfer.smartMode(text, options); + break; + default: + ensureMode(mode); + } + + // Remember the paren tree. + state.parens = result.parens; + + // Remember tab stops for smart tabbing. + state.tabStops = result.tabStops; + + if (text !== result.text) { + // Backup history + const hist = cm.getHistory(); + + // Update text + cm.setValue(result.text); + + // Update cursor and selection + state.monitorCursor = false; + if (hasSelection) { + cm.setSelections(selections); + } else { + cm.setCursor(result.cursorLine, result.cursorX); + } + + // Restore history to avoid pushing our edits to the history stack. + cm.setHistory(hist); + + setTimeout(function () { + state.monitorCursor = true; + }, 0); + + // Update scroll position + cm.scrollTo(scroller.scrollLeft, scroller.scrollTop); + } + + // Clear or add new marks + updateErrorMarks(cm, result.error); + updateParenTrailMarks(cm, result.parenTrails); + + // Remember the cursor position for next time + state.prevCursorLine = result.cursorLine; + state.prevCursorX = result.cursorX; + + if (locus) { + updateLocusLayer(cm, result.parens); + } else if (guides) { + updateGuidesLayer(cm, result.parens); + } + + // Re-run with original mode if code was finally fixed in Paren Mode. + if (state.fixMode && result.success) { + state.fixMode = false; + return fixText(state, changes); + } + + return result.success; + } + +// ------------------------------------------------------------------------------ +// CodeMirror Integration +// ------------------------------------------------------------------------------ + + function onCursorChange(state) { + clearTimeout(state.cursorTimeout); + if (state.monitorCursor) { + state.cursorTimeout = setTimeout(function () { + fixText(state); + }, 0); + } + } + + function onTextChanges(state, changes) { + clearTimeout(state.cursorTimeout); + const origin = changes[0].origin; + if (origin !== 'setValue') { + fixText(state, changes); + } + } + + function on(state) { + if (state.enabled) { + return; + } + state.callbackCursor = function (cm) { + onCursorChange(state); + }; + state.callbackChanges = function (cm, changes) { + onTextChanges(state, changes); + }; + const cm = state.cm; + cm.on('cursorActivity', state.callbackCursor); + cm.on('changes', state.callbackChanges); + state.parinferKeys = { + 'Tab': function (cm) { + onTab(cm, 1); + }, + 'Shift-Tab': function (cm) { + onTab(cm, -1); + } + }; + cm.addKeyMap(state.parinferKeys); + state.enabled = true; + } + + function off(state) { + if (!state.enabled) { + return; + } + const cm = state.cm; + clearAllMarks(cm); + cm.off('cursorActivity', state.callbackCursor); + cm.off('changes', state.callbackChanges); + cm.removeKeyMap(state.parinferKeys); + state.enabled = false; + } + +// ------------------------------------------------------------------------------ +// Public API +// ------------------------------------------------------------------------------ + + function init(cm, mode, options) { + let state = cm[STATE_PROP]; + if (state) { + throw error('init has already been called on this CodeMirror instance'); + } + + mode = mode || SMART_MODE; + ensureMode(mode); + + state = initialState(cm, mode, options); + cm[STATE_PROP] = state; + + state.layer = { + container: getLayerContainer(cm) + }; + return enable(cm); + } + + function enable(cm) { + const state = ensureState(cm); + + // preprocess text to keep Parinfer from changing code structure + if (state.mode !== PAREN_MODE) { + state.fixMode = true; + } + + on(state); + return fixText(state); + } + + function disable(cm) { + const state = ensureState(cm); + off(state); + } + + function setMode(cm, mode) { + const state = ensureState(cm); + ensureMode(mode); + state.mode = mode; + return fixText(state); + } + + function setOptions(cm, options) { + const state = ensureState(cm); + state.options = options; + return fixText(state); + } + + const API = { + version: '1.4.2', + init: init, + enable: enable, + disable: disable, + setMode: setMode, + setOptions: setOptions + }; + + return API; + +}); // end module anonymous scope diff --git a/front_end/dirac/parinfer.js b/front_end/dirac/parinfer.js new file mode 100644 index 0000000000..b125d6649c --- /dev/null +++ b/front_end/dirac/parinfer.js @@ -0,0 +1,1410 @@ +// @ts-nocheck +// Copyright 2020 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. + +// +// Parinfer 3.11.0 +// +// Copyright 2015-2017 © Shaun Lebron +// MIT License +// +// Home Page: http://shaunlebron.github.io/parinfer/ +// GitHub: https://github.com/shaunlebron/parinfer +// +// For DOCUMENTATION on this file, please see `doc/code.md`. +// Use `sync.sh` to keep the function/var links in `doc/code.md` accurate. +// + +// ------------------------------------------------------------------------------ +// JS Module Boilerplate +// ------------------------------------------------------------------------------ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.parinfer = factory(); + } +})(self, function () { // start module anonymous scope + 'use strict'; + +// ------------------------------------------------------------------------------ +// Constants / Predicates +// ------------------------------------------------------------------------------ + +// NOTE: this is a performance hack +// The main result object uses a lot of "unsigned integer or null" values. +// Using a negative integer is faster than actual null because it cuts down on +// type coercion overhead. + const UINT_NULL = -999; + + const INDENT_MODE = 'INDENT_MODE', + PAREN_MODE = 'PAREN_MODE'; + + const BACKSLASH = '\\', + BLANK_SPACE = ' ', + DOUBLE_SPACE = ' ', + DOUBLE_QUOTE = '"', + NEWLINE = '\n', + SEMICOLON = ';', + TAB = '\t'; + + const LINE_ENDING_REGEX = /\r?\n/; + + const MATCH_PAREN = { + '{': '}', + '}': '{', + '[': ']', + ']': '[', + '(': ')', + ')': '(' + }; + +// toggle this to check the asserts during development + const RUN_ASSERTS = false; + + function isBoolean(x) { + return typeof x === 'boolean'; + } + + function isArray(x) { + return Array.isArray(x); + } + + function isInteger(x) { + return typeof x === 'number' && + isFinite(x) && + Math.floor(x) === x; + } + +// ------------------------------------------------------------------------------ +// Options Structure +// ------------------------------------------------------------------------------ + + function transformChange(change) { + if (!change) { + return undefined; + } + + const newLines = change.newText.split(LINE_ENDING_REGEX); + const oldLines = change.oldText.split(LINE_ENDING_REGEX); + + // single line case: + // (defn foo| []) + // ^ newEndX, newEndLineNo + // +++ + + // multi line case: + // (defn foo + // ++++ + // "docstring." + // ++++++++++++++++ + // |[]) + // ++^ newEndX, newEndLineNo + + const lastOldLineLen = oldLines[oldLines.length - 1].length; + const lastNewLineLen = newLines[newLines.length - 1].length; + + const oldEndX = (oldLines.length === 1 ? change.x : 0) + lastOldLineLen; + const newEndX = (newLines.length === 1 ? change.x : 0) + lastNewLineLen; + const newEndLineNo = change.lineNo + (newLines.length - 1); + + return { + x: change.x, + lineNo: change.lineNo, + oldText: change.oldText, + newText: change.newText, + + oldEndX: oldEndX, + newEndX: newEndX, + newEndLineNo: newEndLineNo, + + lookupLineNo: newEndLineNo, + lookupX: newEndX + }; + } + + function transformChanges(changes) { + if (changes.length === 0) { + return null; + } + const lines = {}; + let line, i, change; + for (i = 0; i < changes.length; i++) { + change = transformChange(changes[i]); + line = lines[change.lookupLineNo]; + if (!line) { + line = lines[change.lookupLineNo] = {}; + } + line[change.lookupX] = change; + } + return lines; + } + + function parseOptions(options) { + options = options || {}; + return { + cursorX: options.cursorX, + cursorLine: options.cursorLine, + prevCursorX: options.prevCursorX, + prevCursorLine: options.prevCursorLine, + selectionStartLine: options.selectionStartLine, + changes: options.changes, + partialResult: options.partialResult, + forceBalance: options.forceBalance, + returnParens: options.returnParens + }; + } + +// ------------------------------------------------------------------------------ +// Result Structure +// ------------------------------------------------------------------------------ + +// This represents the running result. As we scan through each character +// of a given text, we mutate this structure to update the state of our +// system. + + function initialParenTrail() { + return { + lineNo: UINT_NULL, // [integer] - line number of the last parsed paren trail + startX: UINT_NULL, // [integer] - x position of first paren in this range + endX: UINT_NULL, // [integer] - x position after the last paren in this range + openers: [], // [array of stack elements] - corresponding open-paren for each close-paren in this range + clamped: { + startX: UINT_NULL, // startX before paren trail was clamped + endX: UINT_NULL, // endX before paren trail was clamped + openers: [] // openers that were cut out after paren trail was clamped + } + }; + } + + function getInitialResult(text, options, mode, smart) { + + const result = { + + mode: mode, // [enum] - current processing mode (INDENT_MODE or PAREN_MODE) + smart: smart, // [boolean] - smart mode attempts special user-friendly behavior + + origText: text, // [string] - original text + origCursorX: UINT_NULL, // [integer] - original cursorX option + origCursorLine: UINT_NULL, // [integer] - original cursorLine option + + inputLines: // [string array] - input lines that we process line-by-line, char-by-char + text.split(LINE_ENDING_REGEX), + inputLineNo: -1, // [integer] - the current input line number + inputX: -1, // [integer] - the current input x position of the current character (ch) + + lines: [], // [string array] - output lines (with corrected parens or indentation) + lineNo: -1, // [integer] - output line number we are on + ch: '', // [string] - character we are processing (can be changed to indicate a replacement) + x: 0, // [integer] - output x position of the current character (ch) + indentX: UINT_NULL, // [integer] - x position of the indentation point if present + + parenStack: [], // We track where we are in the Lisp tree by keeping a stack (array) of open-parens. + // Stack elements are objects containing keys {ch, x, lineNo, indentDelta} + // whose values are the same as those described here in this result structure. + + tabStops: [], // In Indent Mode, it is useful for editors to snap a line's indentation + // to certain critical points. Thus, we have a `tabStops` array of objects containing + // keys {ch, x, lineNo, argX}, which is just the state of the `parenStack` at the cursor line. + + parenTrail: initialParenTrail(), // the range of parens at the end of a line + + parenTrails: [], // [array of {lineNo, startX, endX}] - all non-empty parenTrails to be returned + + returnParens: false, // [boolean] - determines if we return `parens` described below + parens: [], // [array of {lineNo, x, closer, children}] - paren tree if `returnParens` is true + + cursorX: UINT_NULL, // [integer] - x position of the cursor + cursorLine: UINT_NULL, // [integer] - line number of the cursor + prevCursorX: UINT_NULL, // [integer] - x position of the previous cursor + prevCursorLine: UINT_NULL, // [integer] - line number of the previous cursor + + selectionStartLine: UINT_NULL, // [integer] - line number of the current selection starting point + + changes: null, // [object] - mapping change.key to a change object (please see `transformChange` for object structure) + + isInCode: true, // [boolean] - indicates if we are currently in "code space" (not string or comment) + isEscaping: false, // [boolean] - indicates if the next character will be escaped (e.g. `\c`). This may be inside string, comment, or code. + isEscaped: false, // [boolean] - indicates if the current character is escaped (e.g. `\c`). This may be inside string, comment, or code. + isInStr: false, // [boolean] - indicates if we are currently inside a string + isInComment: false, // [boolean] - indicates if we are currently inside a comment + commentX: UINT_NULL, // [integer] - x position of the start of comment on current line (if any) + + quoteDanger: false, // [boolean] - indicates if quotes are imbalanced inside of a comment (dangerous) + trackingIndent: false, // [boolean] - are we looking for the indentation point of the current line? + skipChar: false, // [boolean] - should we skip the processing of the current character? + success: false, // [boolean] - was the input properly formatted enough to create a valid result? + partialResult: false, // [boolean] - should we return a partial result when an error occurs? + forceBalance: false, // [boolean] - should indent mode aggressively enforce paren balance? + + maxIndent: UINT_NULL, // [integer] - maximum allowed indentation of subsequent lines in Paren Mode + indentDelta: 0, // [integer] - how far indentation was shifted by Paren Mode + // (preserves relative indentation of nested expressions) + + trackingArgTabStop: null, // [string] - enum to track how close we are to the first-arg tabStop in a list + // For example a tabStop occurs at `bar` below: + // + // ` (foo bar` + // 00011112222000 <-- state after processing char (enums below) + // + // 0 null => not searching + // 1 'space' => searching for next space + // 2 'arg' => searching for arg + // + // (We create the tabStop when the change from 2->0 happens.) + // + + error: { // if 'success' is false, return this error to the user + name: null, // [string] - Parinfer's unique name for this error + message: null, // [string] - error message to display + lineNo: null, // [integer] - line number of error + x: null, // [integer] - start x position of error + extra: { + name: null, + lineNo: null, + x: null + } + }, + errorPosCache: {} // [object] - maps error name to a potential error position + }; + + // Make sure no new properties are added to the result, for type safety. + // (uncomment only when debugging, since it incurs a perf penalty) + // Object.preventExtensions(result); + // Object.preventExtensions(result.parenTrail); + + // merge options if they are valid + if (options) { + if (isInteger(options.cursorX)) { + result.cursorX = options.cursorX; + result.origCursorX = options.cursorX; + } + if (isInteger(options.cursorLine)) { + result.cursorLine = options.cursorLine; + result.origCursorLine = options.cursorLine; + } + if (isInteger(options.prevCursorX)) { + result.prevCursorX = options.prevCursorX; + } + if (isInteger(options.prevCursorLine)) { + result.prevCursorLine = options.prevCursorLine; + } + if (isInteger(options.selectionStartLine)) { + result.selectionStartLine = options.selectionStartLine; + } + if (isArray(options.changes)) { + result.changes = transformChanges(options.changes); + } + if (isBoolean(options.partialResult)) { + result.partialResult = options.partialResult; + } + if (isBoolean(options.forceBalance)) { + result.forceBalance = options.forceBalance; + } + if (isBoolean(options.returnParens)) { + result.returnParens = options.returnParens; + } + } + + return result; + } + +// ------------------------------------------------------------------------------ +// Possible Errors +// ------------------------------------------------------------------------------ + +// `result.error.name` is set to any of these + const ERROR_QUOTE_DANGER = 'quote-danger'; + const ERROR_EOL_BACKSLASH = 'eol-backslash'; + const ERROR_UNCLOSED_QUOTE = 'unclosed-quote'; + const ERROR_UNCLOSED_PAREN = 'unclosed-paren'; + const ERROR_UNMATCHED_CLOSE_PAREN = 'unmatched-close-paren'; + const ERROR_UNMATCHED_OPEN_PAREN = 'unmatched-open-paren'; + const ERROR_LEADING_CLOSE_PAREN = 'leading-close-paren'; + const ERROR_UNHANDLED = 'unhandled'; + + const errorMessages = {}; + errorMessages[ERROR_QUOTE_DANGER] = 'Quotes must balanced inside comment blocks.'; + errorMessages[ERROR_EOL_BACKSLASH] = 'Line cannot end in a hanging backslash.'; + errorMessages[ERROR_UNCLOSED_QUOTE] = 'String is missing a closing quote.'; + errorMessages[ERROR_UNCLOSED_PAREN] = 'Unclosed open-paren.'; + errorMessages[ERROR_UNMATCHED_CLOSE_PAREN] = 'Unmatched close-paren.'; + errorMessages[ERROR_UNMATCHED_OPEN_PAREN] = 'Unmatched open-paren.'; + errorMessages[ERROR_LEADING_CLOSE_PAREN] = 'Line cannot lead with a close-paren.'; + errorMessages[ERROR_UNHANDLED] = 'Unhandled error.'; + + function cacheErrorPos(result, errorName) { + const e = { + lineNo: result.lineNo, + x: result.x, + inputLineNo: result.inputLineNo, + inputX: result.inputX + }; + result.errorPosCache[errorName] = e; + return e; + } + + function error(result, name) { + let cache = result.errorPosCache[name]; + + const keyLineNo = result.partialResult ? 'lineNo' : 'inputLineNo'; + const keyX = result.partialResult ? 'x' : 'inputX'; + + const e = { + parinferError: true, + name: name, + message: errorMessages[name], + lineNo: cache ? cache[keyLineNo] : result[keyLineNo], + x: cache ? cache[keyX] : result[keyX] + }; + const opener = peek(result.parenStack, 0); + + if (name === ERROR_UNMATCHED_CLOSE_PAREN) { + // extra error info for locating the open-paren that it should've matched + cache = result.errorPosCache[ERROR_UNMATCHED_OPEN_PAREN]; + if (cache || opener) { + e.extra = { + name: ERROR_UNMATCHED_OPEN_PAREN, + lineNo: cache ? cache[keyLineNo] : opener[keyLineNo], + x: cache ? cache[keyX] : opener[keyX] + }; + } + } else if (name === ERROR_UNCLOSED_PAREN) { + e.lineNo = opener[keyLineNo]; + e.x = opener[keyX]; + } + return e; + } + +// ------------------------------------------------------------------------------ +// String Operations +// ------------------------------------------------------------------------------ + + function replaceWithinString(orig, start, end, replace) { + return ( + orig.substring(0, start) + + replace + + orig.substring(end) + ); + } + + if (RUN_ASSERTS) { + console.assert(replaceWithinString('aaa', 0, 2, '') === 'a'); + console.assert(replaceWithinString('aaa', 0, 1, 'b') === 'baa'); + console.assert(replaceWithinString('aaa', 0, 2, 'b') === 'ba'); + } + + function repeatString(text, n) { + let i; + let result = ''; + for (i = 0; i < n; i++) { + result += text; + } + return result; + } + + if (RUN_ASSERTS) { + console.assert(repeatString('a', 2) === 'aa'); + console.assert(repeatString('aa', 3) === 'aaaaaa'); + console.assert(repeatString('aa', 0) === ''); + console.assert(repeatString('', 0) === ''); + console.assert(repeatString('', 5) === ''); + } + + function getLineEnding(text) { + // NOTE: We assume that if the CR char "\r" is used anywhere, + // then we should use CRLF line-endings after every line. + const i = text.search('\r'); + if (i !== -1) { + return '\r\n'; + } + return '\n'; + } + +// ------------------------------------------------------------------------------ +// Line operations +// ------------------------------------------------------------------------------ + + function isCursorAffected(result, start, end) { + if (result.cursorX === start && + result.cursorX === end) { + return result.cursorX === 0; + } + return result.cursorX >= end; + } + + function shiftCursorOnEdit(result, lineNo, start, end, replace) { + const oldLength = end - start; + const newLength = replace.length; + const dx = newLength - oldLength; + + if (dx !== 0 && + result.cursorLine === lineNo && + result.cursorX !== UINT_NULL && + isCursorAffected(result, start, end)) { + result.cursorX += dx; + } + } + + function replaceWithinLine(result, lineNo, start, end, replace) { + const line = result.lines[lineNo]; + const newLine = replaceWithinString(line, start, end, replace); + result.lines[lineNo] = newLine; + + shiftCursorOnEdit(result, lineNo, start, end, replace); + } + + function insertWithinLine(result, lineNo, idx, insert) { + replaceWithinLine(result, lineNo, idx, idx, insert); + } + + function initLine(result, line) { + result.x = 0; + result.lineNo++; + result.lines.push(line); + + // reset line-specific state + result.indentX = UINT_NULL; + result.commentX = UINT_NULL; + result.indentDelta = 0; + delete result.errorPosCache[ERROR_UNMATCHED_CLOSE_PAREN]; + delete result.errorPosCache[ERROR_UNMATCHED_OPEN_PAREN]; + delete result.errorPosCache[ERROR_LEADING_CLOSE_PAREN]; + + result.trackingArgTabStop = null; + result.trackingIndent = !result.isInStr; + } + +// if the current character has changed, commit its change to the current line. + function commitChar(result, origCh) { + const ch = result.ch; + if (origCh !== ch) { + replaceWithinLine(result, result.lineNo, result.x, result.x + origCh.length, ch); + result.indentDelta -= (origCh.length - ch.length); + } + result.x += ch.length; + } + +// ------------------------------------------------------------------------------ +// Misc Utils +// ------------------------------------------------------------------------------ + + function clamp(val, minN, maxN) { + if (minN !== UINT_NULL) { + val = Math.max(minN, val); + } + if (maxN !== UINT_NULL) { + val = Math.min(maxN, val); + } + return val; + } + + if (RUN_ASSERTS) { + console.assert(clamp(1, 3, 5) === 3); + console.assert(clamp(9, 3, 5) === 5); + console.assert(clamp(1, 3, UINT_NULL) === 3); + console.assert(clamp(5, 3, UINT_NULL) === 5); + console.assert(clamp(1, UINT_NULL, 5) === 1); + console.assert(clamp(9, UINT_NULL, 5) === 5); + console.assert(clamp(1, UINT_NULL, UINT_NULL) === 1); + } + + function peek(arr, idxFromBack) { + const maxIdx = arr.length - 1; + if (idxFromBack > maxIdx) { + return null; + } + return arr[maxIdx - idxFromBack]; + } + + if (RUN_ASSERTS) { + console.assert(peek(['a'], 0) === 'a'); + console.assert(peek(['a'], 1) === null); + console.assert(peek(['a', 'b', 'c'], 0) === 'c'); + console.assert(peek(['a', 'b', 'c'], 1) === 'b'); + console.assert(peek(['a', 'b', 'c'], 5) === null); + console.assert(peek([], 0) === null); + console.assert(peek([], 1) === null); + } + +// ------------------------------------------------------------------------------ +// Questions about characters +// ------------------------------------------------------------------------------ + + function isOpenParen(ch) { + return ch === '{' || ch === '(' || ch === '['; + } + + function isCloseParen(ch) { + return ch === '}' || ch === ')' || ch === ']'; + } + + function isValidCloseParen(parenStack, ch) { + if (parenStack.length === 0) { + return false; + } + return peek(parenStack, 0).ch === MATCH_PAREN[ch]; + } + + function isWhitespace(result) { + const ch = result.ch; + return !result.isEscaped && (ch === BLANK_SPACE || ch === DOUBLE_SPACE); + } + +// can this be the last code character of a list? + function isClosable(result) { + const ch = result.ch; + const closer = (isCloseParen(ch) && !result.isEscaped); + return result.isInCode && !isWhitespace(result) && ch !== '' && !closer; + } + +// ------------------------------------------------------------------------------ +// Advanced operations on characters +// ------------------------------------------------------------------------------ + + function checkCursorHolding(result) { + const opener = peek(result.parenStack, 0); + const parent = peek(result.parenStack, 1); + const holdMinX = parent ? parent.x + 1 : 0; + const holdMaxX = opener.x; + + const holding = ( + result.cursorLine === opener.lineNo && + holdMinX <= result.cursorX && result.cursorX <= holdMaxX + ); + const shouldCheckPrev = !result.changes && result.prevCursorLine !== UINT_NULL; + if (shouldCheckPrev) { + const prevHolding = ( + result.prevCursorLine === opener.lineNo && + holdMinX <= result.prevCursorX && result.prevCursorX <= holdMaxX + ); + if (prevHolding && !holding) { + throw {releaseCursorHold: true}; + } + } + return holding; + } + + function trackArgTabStop(result, state) { + if (state === 'space') { + if (result.isInCode && isWhitespace(result)) { + result.trackingArgTabStop = 'arg'; + } + } else if (state === 'arg') { + if (!isWhitespace(result)) { + const opener = peek(result.parenStack, 0); + opener.argX = result.x; + result.trackingArgTabStop = null; + } + } + } + +// ------------------------------------------------------------------------------ +// Literal character events +// ------------------------------------------------------------------------------ + + function onOpenParen(result) { + if (result.isInCode) { + const opener = { + inputLineNo: result.inputLineNo, + inputX: result.inputX, + + lineNo: result.lineNo, + x: result.x, + ch: result.ch, + indentDelta: result.indentDelta, + maxChildIndent: UINT_NULL + }; + + if (result.returnParens) { + opener.children = []; + opener.closer = { + lineNo: UINT_NULL, + x: UINT_NULL, + ch: '' + }; + let parent = peek(result.parenStack, 0); + parent = parent ? parent.children : result.parens; + parent.push(opener); + } + + result.parenStack.push(opener); + result.trackingArgTabStop = 'space'; + } + } + + function setCloser(opener, lineNo, x, ch) { + opener.closer.lineNo = lineNo; + opener.closer.x = x; + opener.closer.ch = ch; + } + + function onMatchedCloseParen(result) { + const opener = peek(result.parenStack, 0); + if (result.returnParens) { + setCloser(opener, result.lineNo, result.x, result.ch); + } + + result.parenTrail.endX = result.x + 1; + result.parenTrail.openers.push(opener); + + if (result.mode === INDENT_MODE && result.smart && checkCursorHolding(result)) { + const origStartX = result.parenTrail.startX; + const origEndX = result.parenTrail.endX; + const origOpeners = result.parenTrail.openers; + resetParenTrail(result, result.lineNo, result.x + 1); + result.parenTrail.clamped.startX = origStartX; + result.parenTrail.clamped.endX = origEndX; + result.parenTrail.clamped.openers = origOpeners; + } + result.parenStack.pop(); + result.trackingArgTabStop = null; + } + + function onUnmatchedCloseParen(result) { + if (result.mode === PAREN_MODE) { + const trail = result.parenTrail; + const inLeadingParenTrail = trail.lineNo === result.lineNo && trail.startX === result.indentX; + const canRemove = result.smart && inLeadingParenTrail; + if (!canRemove) { + throw error(result, ERROR_UNMATCHED_CLOSE_PAREN); + } + } else if (result.mode === INDENT_MODE && !result.errorPosCache[ERROR_UNMATCHED_CLOSE_PAREN]) { + cacheErrorPos(result, ERROR_UNMATCHED_CLOSE_PAREN); + const opener = peek(result.parenStack, 0); + if (opener) { + const e = cacheErrorPos(result, ERROR_UNMATCHED_OPEN_PAREN); + e.inputLineNo = opener.inputLineNo; + e.inputX = opener.inputX; + } + } + result.ch = ''; + } + + function onCloseParen(result) { + if (result.isInCode) { + if (isValidCloseParen(result.parenStack, result.ch)) { + onMatchedCloseParen(result); + } else { + onUnmatchedCloseParen(result); + } + } + } + + function onTab(result) { + if (result.isInCode) { + result.ch = DOUBLE_SPACE; + } + } + + function onSemicolon(result) { + if (result.isInCode) { + result.isInComment = true; + result.commentX = result.x; + result.trackingArgTabStop = null; + } + } + + function onNewline(result) { + result.isInComment = false; + result.ch = ''; + } + + function onQuote(result) { + if (result.isInStr) { + result.isInStr = false; + } else if (result.isInComment) { + result.quoteDanger = !result.quoteDanger; + if (result.quoteDanger) { + cacheErrorPos(result, ERROR_QUOTE_DANGER); + } + } else { + result.isInStr = true; + cacheErrorPos(result, ERROR_UNCLOSED_QUOTE); + } + } + + function onBackslash(result) { + result.isEscaping = true; + } + + function afterBackslash(result) { + result.isEscaping = false; + result.isEscaped = true; + + if (result.ch === NEWLINE) { + if (result.isInCode) { + throw error(result, ERROR_EOL_BACKSLASH); + } + onNewline(result); + } + } + +// ------------------------------------------------------------------------------ +// Character dispatch +// ------------------------------------------------------------------------------ + + function onChar(result) { + let ch = result.ch; + result.isEscaped = false; + + if (result.isEscaping) { + afterBackslash(result); + } else if (isOpenParen(ch)) { + onOpenParen(result); + } else if (isCloseParen(ch)) { + onCloseParen(result); + } else if (ch === DOUBLE_QUOTE) { + onQuote(result); + } else if (ch === SEMICOLON) { + onSemicolon(result); + } else if (ch === BACKSLASH) { + onBackslash(result); + } else if (ch === TAB) { + onTab(result); + } else if (ch === NEWLINE) { + onNewline(result); + } + + ch = result.ch; + + result.isInCode = !result.isInComment && !result.isInStr; + + if (isClosable(result)) { + resetParenTrail(result, result.lineNo, result.x + ch.length); + } + + const state = result.trackingArgTabStop; + if (state) { + trackArgTabStop(result, state); + } + } + +// ------------------------------------------------------------------------------ +// Cursor functions +// ------------------------------------------------------------------------------ + + function isCursorLeftOf(cursorX, cursorLine, x, lineNo) { + return ( + cursorLine === lineNo && + x !== UINT_NULL && + cursorX !== UINT_NULL && + cursorX <= x // inclusive since (cursorX = x) implies (x-1 < cursor < x) + ); + } + + function isCursorRightOf(cursorX, cursorLine, x, lineNo) { + return ( + cursorLine === lineNo && + x !== UINT_NULL && + cursorX !== UINT_NULL && + cursorX > x + ); + } + + function isCursorInComment(result, cursorX, cursorLine) { + return isCursorRightOf(cursorX, cursorLine, result.commentX, result.lineNo); + } + + function handleChangeDelta(result) { + if (result.changes && (result.smart || result.mode === PAREN_MODE)) { + const line = result.changes[result.inputLineNo]; + if (line) { + const change = line[result.inputX]; + if (change) { + result.indentDelta += (change.newEndX - change.oldEndX); + } + } + } + } + +// ------------------------------------------------------------------------------ +// Paren Trail functions +// ------------------------------------------------------------------------------ + + function resetParenTrail(result, lineNo, x) { + result.parenTrail.lineNo = lineNo; + result.parenTrail.startX = x; + result.parenTrail.endX = x; + result.parenTrail.openers = []; + result.parenTrail.clamped.startX = UINT_NULL; + result.parenTrail.clamped.endX = UINT_NULL; + result.parenTrail.clamped.openers = []; + } + + function isCursorClampingParenTrail(result, cursorX, cursorLine) { + return ( + isCursorRightOf(cursorX, cursorLine, result.parenTrail.startX, result.lineNo) && + !isCursorInComment(result, cursorX, cursorLine) + ); + } + +// INDENT MODE: allow the cursor to clamp the paren trail + function clampParenTrailToCursor(result) { + const startX = result.parenTrail.startX; + const endX = result.parenTrail.endX; + + const clamping = isCursorClampingParenTrail(result, result.cursorX, result.cursorLine); + + if (clamping) { + const newStartX = Math.max(startX, result.cursorX); + const newEndX = Math.max(endX, result.cursorX); + + const line = result.lines[result.lineNo]; + let removeCount = 0; + let i; + for (i = startX; i < newStartX; i++) { + if (isCloseParen(line[i])) { + removeCount++; + } + } + + const openers = result.parenTrail.openers; + + result.parenTrail.openers = openers.slice(removeCount); + result.parenTrail.startX = newStartX; + result.parenTrail.endX = newEndX; + + result.parenTrail.clamped.openers = openers.slice(0, removeCount); + result.parenTrail.clamped.startX = startX; + result.parenTrail.clamped.endX = endX; + } + } + +// INDENT MODE: pops the paren trail from the stack + function popParenTrail(result) { + const startX = result.parenTrail.startX; + const endX = result.parenTrail.endX; + + if (startX === endX) { + return; + } + + const openers = result.parenTrail.openers; + while (openers.length !== 0) { + result.parenStack.push(openers.pop()); + } + } + + function getParentOpenerIndex(result, indentX) { + let i; + for (i = 0; i < result.parenStack.length; i++) { + const opener = peek(result.parenStack, i); + const currOutside = (opener.x < indentX); + const prevOutside = (opener.x - opener.indentDelta < indentX); + + if (prevOutside) { + // If an open-paren WAS outside, its `indentDelta` will be used to KEEP IT + // outside, by adjusting the indentation of its child lines. + break; + } + if (currOutside) { + // If an open-paren was JUST pushed outside and its parent open-paren was + // not pushed by same amount, new child line(s) will be adopted. + // Clear `indentDelta` since it is reserved for previous child lines only. + const nextOpener = peek(result.parenStack, i + 1); + if (!nextOpener || nextOpener.indentDelta !== opener.indentDelta) { + opener.indentDelta = 0; + break; + } + } + } + return i; + } + +// INDENT MODE: correct paren trail from indentation + function correctParenTrail(result, indentX) { + let parens = ''; + + const index = getParentOpenerIndex(result, indentX); + let i; + for (i = 0; i < index; i++) { + const opener = result.parenStack.pop(); + result.parenTrail.openers.push(opener); + const closeCh = MATCH_PAREN[opener.ch]; + parens += closeCh; + + if (result.returnParens) { + setCloser(opener, result.parenTrail.lineNo, result.parenTrail.startX + i, closeCh); + } + } + + if (result.parenTrail.lineNo !== UINT_NULL) { + replaceWithinLine(result, result.parenTrail.lineNo, result.parenTrail.startX, result.parenTrail.endX, parens); + result.parenTrail.endX = result.parenTrail.startX + parens.length; + rememberParenTrail(result); + } + } + +// PAREN MODE: remove spaces from the paren trail + function cleanParenTrail(result) { + const startX = result.parenTrail.startX; + const endX = result.parenTrail.endX; + + if (startX === endX || + result.lineNo !== result.parenTrail.lineNo) { + return; + } + + const line = result.lines[result.lineNo]; + let newTrail = ''; + let spaceCount = 0; + let i; + for (i = startX; i < endX; i++) { + if (isCloseParen(line[i])) { + newTrail += line[i]; + } else { + spaceCount++; + } + } + + if (spaceCount > 0) { + replaceWithinLine(result, result.lineNo, startX, endX, newTrail); + result.parenTrail.endX -= spaceCount; + } + } + +// PAREN MODE: append a valid close-paren to the end of the paren trail + function appendParenTrail(result) { + const opener = result.parenStack.pop(); + const closeCh = MATCH_PAREN[opener.ch]; + if (result.returnParens) { + setCloser(opener, result.parenTrail.lineNo, result.parenTrail.endX, closeCh); + } + + setMaxIndent(result, opener); + insertWithinLine(result, result.parenTrail.lineNo, result.parenTrail.endX, closeCh); + + result.parenTrail.endX++; + result.parenTrail.openers.push(opener); + updateRememberedParenTrail(result); + } + + function invalidateParenTrail(result) { + result.parenTrail = initialParenTrail(); + } + + function checkUnmatchedOutsideParenTrail(result) { + const cache = result.errorPosCache[ERROR_UNMATCHED_CLOSE_PAREN]; + if (cache && cache.x < result.parenTrail.startX) { + throw error(result, ERROR_UNMATCHED_CLOSE_PAREN); + } + } + + function setMaxIndent(result, opener) { + if (opener) { + const parent = peek(result.parenStack, 0); + if (parent) { + parent.maxChildIndent = opener.x; + } else { + result.maxIndent = opener.x; + } + } + } + + function rememberParenTrail(result) { + const trail = result.parenTrail; + const openers = trail.clamped.openers.concat(trail.openers); + if (openers.length > 0) { + const isClamped = trail.clamped.startX !== UINT_NULL; + const allClamped = trail.openers.length === 0; + const shortTrail = { + lineNo: trail.lineNo, + startX: isClamped ? trail.clamped.startX : trail.startX, + endX: allClamped ? trail.clamped.endX : trail.endX + }; + result.parenTrails.push(shortTrail); + + if (result.returnParens) { + let i; + for (i = 0; i < openers.length; i++) { + openers[i].closer.trail = shortTrail; + } + } + } + } + + function updateRememberedParenTrail(result) { + const trail = result.parenTrails[result.parenTrails.length - 1]; + if (!trail || trail.lineNo !== result.parenTrail.lineNo) { + rememberParenTrail(result); + } else { + trail.endX = result.parenTrail.endX; + if (result.returnParens) { + const opener = result.parenTrail.openers[result.parenTrail.openers.length - 1]; + opener.closer.trail = trail; + } + } + } + + function finishNewParenTrail(result) { + if (result.isInStr) { + invalidateParenTrail(result); + } else if (result.mode === INDENT_MODE) { + clampParenTrailToCursor(result); + popParenTrail(result); + } else if (result.mode === PAREN_MODE) { + setMaxIndent(result, peek(result.parenTrail.openers, 0)); + if (result.lineNo !== result.cursorLine) { + cleanParenTrail(result); + } + rememberParenTrail(result); + } + } + +// ------------------------------------------------------------------------------ +// Indentation functions +// ------------------------------------------------------------------------------ + + function addIndent(result, delta) { + const origIndent = result.x; + const newIndent = origIndent + delta; + const indentStr = repeatString(BLANK_SPACE, newIndent); + replaceWithinLine(result, result.lineNo, 0, origIndent, indentStr); + result.x = newIndent; + result.indentX = newIndent; + result.indentDelta += delta; + } + + function shouldAddOpenerIndent(result, opener) { + // Don't add opener.indentDelta if the user already added it. + // (happens when multiple lines are indented together) + return (opener.indentDelta !== result.indentDelta); + } + + function correctIndent(result) { + const origIndent = result.x; + let newIndent = origIndent; + let minIndent = 0; + let maxIndent = result.maxIndent; + + const opener = peek(result.parenStack, 0); + if (opener) { + minIndent = opener.x + 1; + maxIndent = opener.maxChildIndent; + if (shouldAddOpenerIndent(result, opener)) { + newIndent += opener.indentDelta; + } + } + + newIndent = clamp(newIndent, minIndent, maxIndent); + + if (newIndent !== origIndent) { + addIndent(result, newIndent - origIndent); + } + } + + function onIndent(result) { + result.indentX = result.x; + result.trackingIndent = false; + + if (result.quoteDanger) { + throw error(result, ERROR_QUOTE_DANGER); + } + + if (result.mode === INDENT_MODE) { + correctParenTrail(result, result.x); + + const opener = peek(result.parenStack, 0); + if (opener && shouldAddOpenerIndent(result, opener)) { + addIndent(result, opener.indentDelta); + } + } else if (result.mode === PAREN_MODE) { + correctIndent(result); + } + } + + function checkLeadingCloseParen(result) { + if (result.errorPosCache[ERROR_LEADING_CLOSE_PAREN] && + result.parenTrail.lineNo === result.lineNo) { + throw error(result, ERROR_LEADING_CLOSE_PAREN); + } + } + + function onLeadingCloseParen(result) { + if (result.mode === INDENT_MODE) { + if (!result.forceBalance) { + if (result.smart) { + throw {leadingCloseParen: true}; + } + if (!result.errorPosCache[ERROR_LEADING_CLOSE_PAREN]) { + cacheErrorPos(result, ERROR_LEADING_CLOSE_PAREN); + } + } + result.skipChar = true; + } + if (result.mode === PAREN_MODE) { + if (!isValidCloseParen(result.parenStack, result.ch)) { + if (result.smart) { + result.skipChar = true; + } else { + throw error(result, ERROR_UNMATCHED_CLOSE_PAREN); + } + } else if (isCursorLeftOf(result.cursorX, result.cursorLine, result.x, result.lineNo)) { + resetParenTrail(result, result.lineNo, result.x); + onIndent(result); + } else { + appendParenTrail(result); + result.skipChar = true; + } + } + } + + function onCommentLine(result) { + const parenTrailLength = result.parenTrail.openers.length; + + // restore the openers matching the previous paren trail + let j; + if (result.mode === PAREN_MODE) { + for (j = 0; j < parenTrailLength; j++) { + result.parenStack.push(peek(result.parenTrail.openers, j)); + } + } + + const i = getParentOpenerIndex(result, result.x); + const opener = peek(result.parenStack, i); + if (opener) { + // shift the comment line based on the parent open paren + if (shouldAddOpenerIndent(result, opener)) { + addIndent(result, opener.indentDelta); + } + // TODO: store some information here if we need to place close-parens after comment lines + } + + // repop the openers matching the previous paren trail + if (result.mode === PAREN_MODE) { + for (j = 0; j < parenTrailLength; j++) { + result.parenStack.pop(); + } + } + } + + function checkIndent(result) { + if (isCloseParen(result.ch)) { + onLeadingCloseParen(result); + } else if (result.ch === SEMICOLON) { + // comments don't count as indentation points + onCommentLine(result); + result.trackingIndent = false; + } else if (result.ch !== NEWLINE && + result.ch !== BLANK_SPACE && + result.ch !== TAB) { + onIndent(result); + } + } + + function makeTabStop(result, opener) { + const tabStop = { + ch: opener.ch, + x: opener.x, + lineNo: opener.lineNo + }; + if (opener.argX != null) { + tabStop.argX = opener.argX; + } + return tabStop; + } + + function getTabStopLine(result) { + return result.selectionStartLine !== UINT_NULL ? result.selectionStartLine : result.cursorLine; + } + + function setTabStops(result) { + if (getTabStopLine(result) !== result.lineNo) { + return; + } + + let i; + for (i = 0; i < result.parenStack.length; i++) { + result.tabStops.push(makeTabStop(result, result.parenStack[i])); + } + + if (result.mode === PAREN_MODE) { + for (i = result.parenTrail.openers.length - 1; i >= 0; i--) { + result.tabStops.push(makeTabStop(result, result.parenTrail.openers[i])); + } + } + + // remove argX if it falls to the right of the next stop + for (i = 1; i < result.tabStops.length; i++) { + const x = result.tabStops[i].x; + const prevArgX = result.tabStops[i - 1].argX; + if (prevArgX != null && prevArgX >= x) { + delete result.tabStops[i - 1].argX; + } + } + } + +// ------------------------------------------------------------------------------ +// High-level processing functions +// ------------------------------------------------------------------------------ + + function processChar(result, ch) { + const origCh = ch; + + result.ch = ch; + result.skipChar = false; + + handleChangeDelta(result); + + if (result.trackingIndent) { + checkIndent(result); + } + + if (result.skipChar) { + result.ch = ''; + } else { + onChar(result); + } + + commitChar(result, origCh); + } + + function processLine(result, lineNo) { + initLine(result, result.inputLines[lineNo]); + + setTabStops(result); + + let x; + for (x = 0; x < result.inputLines[lineNo].length; x++) { + result.inputX = x; + processChar(result, result.inputLines[lineNo][x]); + } + processChar(result, NEWLINE); + + if (!result.forceBalance) { + checkUnmatchedOutsideParenTrail(result); + checkLeadingCloseParen(result); + } + + if (result.lineNo === result.parenTrail.lineNo) { + finishNewParenTrail(result); + } + } + + function finalizeResult(result) { + if (result.quoteDanger) { + throw error(result, ERROR_QUOTE_DANGER); + } + if (result.isInStr) { + throw error(result, ERROR_UNCLOSED_QUOTE); + } + + if (result.parenStack.length !== 0) { + if (result.mode === PAREN_MODE) { + throw error(result, ERROR_UNCLOSED_PAREN); + } + } + if (result.mode === INDENT_MODE) { + result.x = 0; + onIndent(result); + } + result.success = true; + } + + function processError(result, e) { + result.success = false; + if (e.parinferError) { + delete e.parinferError; + result.error = e; + } else { + result.error.name = ERROR_UNHANDLED; + result.error.message = e.stack; + throw e; + } + } + + function processText(text, options, mode, smart) { + const result = getInitialResult(text, options, mode, smart); + + try { + let i; + for (i = 0; i < result.inputLines.length; i++) { + result.inputLineNo = i; + processLine(result, i); + } + finalizeResult(result); + } catch (e) { + if (e.leadingCloseParen || e.releaseCursorHold) { + return processText(text, options, PAREN_MODE, smart); + } + processError(result, e); + } + + return result; + } + +// ------------------------------------------------------------------------------ +// Public API +// ------------------------------------------------------------------------------ + + function publicResult(result) { + const lineEnding = getLineEnding(result.origText); + let final; + if (result.success) { + final = { + text: result.lines.join(lineEnding), + cursorX: result.cursorX, + cursorLine: result.cursorLine, + success: true, + tabStops: result.tabStops, + parenTrails: result.parenTrails + }; + if (result.returnParens) { + final.parens = result.parens; + } + } else { + final = { + text: result.partialResult ? result.lines.join(lineEnding) : result.origText, + cursorX: result.partialResult ? result.cursorX : result.origCursorX, + cursorLine: result.partialResult ? result.cursorLine : result.origCursorLine, + parenTrails: result.partialResult ? result.parenTrails : null, + success: false, + error: result.error + }; + if (result.partialResult && result.returnParens) { + final.parens = result.parens; + } + } + if (final.cursorX === UINT_NULL) { + delete final.cursorX; + } + if (final.cursorLine === UINT_NULL) { + delete final.cursorLine; + } + if (final.tabStops && final.tabStops.length === 0) { + delete final.tabStops; + } + return final; + } + + function indentMode(text, options) { + options = parseOptions(options); + return publicResult(processText(text, options, INDENT_MODE)); + } + + function parenMode(text, options) { + options = parseOptions(options); + return publicResult(processText(text, options, PAREN_MODE)); + } + + function smartMode(text, options) { + options = parseOptions(options); + const smart = options.selectionStartLine == null; + return publicResult(processText(text, options, INDENT_MODE, smart)); + } + + const API = { + version: '3.11.0', + indentMode: indentMode, + parenMode: parenMode, + smartMode: smartMode + }; + + return API; + +}); // end module anonymous scope diff --git a/front_end/dirac/require-implant.js b/front_end/dirac/require-implant.js new file mode 100644 index 0000000000..72d59c0363 --- /dev/null +++ b/front_end/dirac/require-implant.js @@ -0,0 +1,32 @@ +// @ts-nocheck +// Copyright 2020 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. + +if (typeof runtime !== 'undefined') { + // this code runs only in dev mode + // we want to avoid tweaking inspector.html + (function (d, script) { + const insertScript = function (url, f) { + script = d.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + if (f) { + script.onload = f; + } + script.src = url; + d.getElementsByTagName('head')[0].appendChild(script); + }; + + insertScript('dirac/.compiled/implant/goog/base.js', function () { + goog.define('goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING', true); + goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING = true; + insertScript('dirac/.compiled/implant/goog/deps.js', function () { + insertScript('dirac/.compiled/implant/cljs_deps.js', function () { + goog.require('dirac.devtools'); + goog.require('dirac.implant'); + }); + }); + }); + })(document); +} diff --git a/front_end/dirac_lazy/dirac_lazy.js b/front_end/dirac_lazy/dirac_lazy.js new file mode 100644 index 0000000000..16d4288d4d --- /dev/null +++ b/front_end/dirac_lazy/dirac_lazy.js @@ -0,0 +1,974 @@ +// @ts-nocheck +// Copyright 2020 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. + +console.log('dirac-lazy module import!'); + +if (!window.dirac) { + console.error('window.dirac was expected to exist when loading dirac_lazy overlay'); + throw new Error('window.dirac was expected to exist when loading dirac_lazy overlay'); +} + +Object.assign(window.dirac, (function () { + + const namespacesSymbolsCache = new Map(); + + // --- eval support ----------------------------------------------------------------------------------------------------- + + function lookupCurrentContext() { + return self.UI.context.flavor(SDK.ExecutionContext); + } + + function evalInContext(context, code, silent, callback) { + if (!context) { + console.warn('Requested evalInContext with null context:', code); + return; + } + const resultCallback = function (result, exceptionDetails) { + if (dirac.DEBUG_EVAL) { + console.log('evalInContext/resultCallback: result', result, 'exceptionDetails', exceptionDetails); + } + if (callback) { + let exceptionDescription = null; + if (exceptionDetails) { + const exception = exceptionDetails.exception; + if (exception) { + exceptionDescription = exception.description; + } + if (!exceptionDescription) { + exceptionDescription = exceptionDetails.text; + } + if (!exceptionDescription) { + exceptionDescription = '?'; + } + } + + callback(result, exceptionDescription); + } + }; + try { + if (dirac.DEBUG_EVAL) { + console.log('evalInContext', context, silent, code); + } + context.evaluate({ + expression: code, + objectGroup: 'console', + includeCommandLineAPI: true, + silent: silent, + returnByValue: true, + generatePreview: false + }, false, false).then(answer => resultCallback(answer.object, answer.exceptionDetails)); + } catch (e) { + console.error('failed js evaluation in context:', context, 'code', code); + } + } + + function hasCurrentContext() { + return !!lookupCurrentContext(); + } + + function evalInCurrentContext(code, silent, callback) { + if (dirac.DEBUG_EVAL) { + console.log('evalInCurrentContext called:', code, silent, callback); + } + evalInContext(lookupCurrentContext(), code, silent, callback); + } + + function lookupDefaultContext() { + if (dirac.DEBUG_EVAL) { + console.log('lookupDefaultContext called'); + } + if (!SDK.targetManager) { + if (dirac.DEBUG_EVAL) { + console.log(' !SDK.targetManager => bail out'); + } + return null; + } + const target = SDK.targetManager.mainTarget(); + if (!target) { + if (dirac.DEBUG_EVAL) { + console.log(' !target => bail out'); + } + return null; + } + const runtimeModel = target.model(SDK.RuntimeModel); + if (!runtimeModel) { + if (dirac.DEBUG_EVAL) { + console.log(' !runtimeModel => bail out'); + } + return null; + } + const executionContexts = runtimeModel.executionContexts(); + if (dirac.DEBUG_EVAL) { + console.log(' execution contexts:', executionContexts); + } + for (let i = 0; i < executionContexts.length; ++i) { + const executionContext = executionContexts[i]; + if (executionContext.isDefault) { + if (dirac.DEBUG_EVAL) { + console.log(' execution context #' + i + ' isDefault:', executionContext); + } + return executionContext; + } + } + if (executionContexts.length > 0) { + if (dirac.DEBUG_EVAL) { + console.log(' lookupDefaultContext failed to find valid context => return the first one'); + } + return executionContexts[0]; + } + if (dirac.DEBUG_EVAL) { + console.log(' lookupDefaultContext failed to find valid context => no context avail'); + } + return null; + } + + function hasDefaultContext() { + return !!lookupDefaultContext(); + } + + function evalInDefaultContext(code, silent, callback) { + if (dirac.DEBUG_EVAL) { + console.log('evalInDefaultContext called:', code, silent, callback); + } + evalInContext(lookupDefaultContext(), code, silent, callback); + } + + function getMainDebuggerModel() { + return SDK.targetManager.mainTarget().model(SDK.DebuggerModel); + } + + const debuggerEventsUnsubscribers = new Map(); + + /** + * @return {boolean} + */ + function subscribeDebuggerEvents(callback) { + if (debuggerEventsUnsubscribers.has(callback)) { + throw new Error('subscribeDebuggerEvents called without prior unsubscribeDebuggerEvents for callback ' + callback); + } + const globalObjectClearedHandler = (...args) => { + callback('GlobalObjectCleared', ...args); + }; + const debuggerPausedHandler = (...args) => { + callback('DebuggerPaused', ...args); + }; + const debuggerResumedHandler = (...args) => { + callback('DebuggerResumed', ...args); + }; + + SDK.targetManager.addModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, globalObjectClearedHandler, window.dirac); + SDK.targetManager.addModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, debuggerPausedHandler, window.dirac); + SDK.targetManager.addModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, debuggerResumedHandler, window.dirac); + + debuggerEventsUnsubscribers.set(callback, () => { + SDK.targetManager.removeModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, globalObjectClearedHandler, window.dirac); + SDK.targetManager.removeModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, debuggerPausedHandler, window.dirac); + SDK.targetManager.removeModelListener(SDK.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, debuggerResumedHandler, window.dirac); + return true; + }); + + return true; + } + + /** + * @return {boolean} + */ + function unsubscribeDebuggerEvents(callback) { + if (!debuggerEventsUnsubscribers.has(callback)) { + throw new Error('unsubscribeDebuggerEvents called without prior subscribeDebuggerEvents for callback ' + callback); + } + + const unsubscriber = debuggerEventsUnsubscribers.get(callback); + debuggerEventsUnsubscribers.delete(callback); + return unsubscriber(); + } + + // --- console ---------------------------------------------------------------------------------------------------------- + + function addConsoleMessageToMainTarget(type, level, text, parameters) { + const target = SDK.targetManager.mainTarget(); + if (!target) { + console.warn('Unable to add console message to main target (no target): ', text); + return; + } + const runtimeModel = target.model(SDK.RuntimeModel); + if (!runtimeModel) { + console.warn('Unable to add console message to main target (no runtime model): ', text); + return; + } + const sanitizedText = text || ''; + const msg = new SDK.ConsoleMessage(runtimeModel, SDK.ConsoleMessage.MessageSource.Other, level, + sanitizedText, type, undefined, undefined, undefined, parameters); + SDK.consoleModel.addMessage(msg); + } + + function evaluateCommandInConsole(contextName, code) { + const context = contextName === 'current' ? lookupCurrentContext() : lookupDefaultContext(); + if (!context) { + console.warn("evaluateCommandInConsole got null '" + contextName + "' context:", code); + return; + } + const commandMessage = new SDK.ConsoleMessage(context.runtimeModel, SDK.ConsoleMessage.MessageSource.JS, null, code, SDK.ConsoleMessage.MessageType.Command); + commandMessage.setExecutionContextId(context.id); + commandMessage.skipHistory = true; + SDK.consoleModel.evaluateCommandInConsole(context, commandMessage, code, false); + } + + // --- scope info ------------------------------------------------------------------------------------------------------- + + function getScopeTitle(scope) { + let title = null; + + switch (scope.type()) { + case Protocol.Debugger.ScopeType.Local: + title = Common.UIString('Local'); + break; + case Protocol.Debugger.ScopeType.Closure: + const scopeName = scope.name(); + if (scopeName) { + title = Common.UIString('Closure (%s)', UI.beautifyFunctionName(scopeName)); + } else { + title = Common.UIString('Closure'); + } + break; + case Protocol.Debugger.ScopeType.Catch: + title = Common.UIString('Catch'); + break; + case Protocol.Debugger.ScopeType.Block: + title = Common.UIString('Block'); + break; + case Protocol.Debugger.ScopeType.Script: + title = Common.UIString('Script'); + break; + case Protocol.Debugger.ScopeType.With: + title = Common.UIString('With Block'); + break; + case Protocol.Debugger.ScopeType.Global: + title = Common.UIString('Global'); + break; + } + + return title; + } + + function extractNamesFromScopePromise(scope) { + const title = getScopeTitle(scope); + const remoteObject = Sources.SourceMapNamesResolver.resolveScopeInObject(scope); + + const result = {title: title}; + let resolved = false; + + return new Promise(function (resolve) { + + function processProperties(answer) { + const properties = answer.properties; + if (properties) { + result.props = properties.map(function (property) { + const propertyRecord = {name: property.name}; + if (property.resolutionSourceProperty) { + const identifier = property.resolutionSourceProperty.name; + if (identifier !== property.name) { + propertyRecord.identifier = identifier; + } + } + return propertyRecord; + }); + } + + resolved = true; + resolve(result); + } + + function timeoutProperties() { + if (resolved) { + return; + } + console.warn('Unable to retrieve properties from remote object', remoteObject); + resolve(result); + } + + remoteObject.getAllProperties(false, false).then(processProperties); + setTimeout(timeoutProperties, dirac.REMOTE_OBJECT_PROPERTIES_FETCH_TIMEOUT); + }); + } + + function extractScopeInfoFromScopeChainAsync(callFrame) { + if (!callFrame) { + return Promise.resolve(null); + } + + return new Promise(function (resolve) { + const scopeNamesPromises = []; + + const scopeChain = callFrame.scopeChain(); + for (let i = 0; i < scopeChain.length; ++i) { + const scope = scopeChain[i]; + if (scope.type() === Protocol.Debugger.ScopeType.Global) { + continue; + } + + scopeNamesPromises.unshift(extractNamesFromScopePromise(scope)); + } + + Promise.all(scopeNamesPromises).then(function (frames) { + const result = {frames: frames}; + resolve(result); + }); + }); + } + + // --- helpers ---------------------------------------------------------------------------------------------------------- + + /** + * @param {string} namespaceName + * @return {function(string)} + */ + function prepareUrlMatcher(namespaceName) { + // shadow-cljs uses slightly different convention to output files + // for example given namespaceName 'my.cool.ns' + // standard clojurescript outputs into directory structure $some-prefix/my/cool/ns.js + // cljs files are placed under the same names + // + // shadow-cljs outputs into flat directory structure cljs-runtime/my.cool.ns.js + // but shadow-cljs maintains tree-like structure for original cljs sources, similar to standard + // + const relativeNSPathStandard = dirac.nsToRelpath(namespaceName, 'js'); + const relativeNSPathShadow = relativeNSPathStandard.replace('/', '.'); + const parser = document.createElement('a'); + return /** @suppressGlobalPropertiesCheck */ function (url) { + parser.href = url; + // console.log("URL MATCH", relativeNSPathShadow, parser.pathname); + return parser.pathname.endsWith(relativeNSPathStandard) || parser.pathname.endsWith(relativeNSPathShadow); + }; + } + + function unique(a) { + return Array.from(new Set(a)); + } + + function isRelevantSourceCode(uiSourceCode) { + return uiSourceCode.contentType().isScript() && !uiSourceCode.contentType().isFromSourceMap() && + uiSourceCode.project().type() === Workspace.projectTypes.Network; + } + + function getRelevantSourceCodes(workspace) { + return workspace.uiSourceCodes().filter(isRelevantSourceCode); + } + + // --- parsing namespaces ----------------------------------------------------------------------------------------------- + + /** + * @param {string} url + * @param {string} cljsSourceCode + * @return {!Array} + */ + function parseClojureScriptNamespaces(url, cljsSourceCode) { + if (dirac.DEBUG_CACHES) { + console.groupCollapsed("parseClojureScriptNamespaces: " + url); + console.log(cljsSourceCode); + console.groupEnd(); + } + if (!cljsSourceCode) { + console.warn('unexpected empty source from ' + url); + return []; + } + const descriptor = dirac.parseNsFromSource(cljsSourceCode); + if (!descriptor) { + return []; + } + + descriptor.url = url; + return [descriptor]; + } + + /** + * @param {string} url + * @param {?string} jsSourceCode + * @return {!Array} + */ + function parsePseudoNamespaces(url, jsSourceCode) { + if (dirac.DEBUG_CACHES) { + console.groupCollapsed("parsePseudoNamespaces: " + url); + console.log(jsSourceCode); + console.groupEnd(); + } + if (!jsSourceCode) { + console.warn('unexpected empty source from ' + url); + return []; + } + + const result = []; + // standard clojurescript emits: goog.provide('goog.something'); + // shadow-cljs emits: goog.module("goog.something"); + const re = /goog\.(provide|module)\(['"](.*?)['"]\);/gm; + let m; + while (m = re.exec(jsSourceCode)) { + const namespaceName = m[2]; + const descriptor = { + name: namespaceName, + url: url, + pseudo: true + }; + result.push(descriptor); + } + + return result; + } + + function ensureSourceMapLoadedAsync(script) { + if (!script.sourceMapURL) { + return Promise.resolve(null); + } + const sourceMap = Bindings.debuggerWorkspaceBinding.sourceMapForScript(script); + if (sourceMap) { + return Promise.resolve(sourceMap); + } + return new Promise(resolve => { + let counter = 0; + const interval = setInterval(() => { + const sourceMap = Bindings.debuggerWorkspaceBinding.sourceMapForScript(script); + if (sourceMap) { + clearInterval(interval); + resolve(sourceMap); + } + counter += 1; + if (counter > 100) { // 10s + clearInterval(interval); + console.warn("source map didn't load in time for", script); + resolve(null); + } + }, 100); + }); + } + + /** + * @param {!SDK.Script} script + * @return {!Promise>} + * @suppressGlobalPropertiesCheck + */ + function parseNamespacesDescriptorsAsync(script) { + if (script.isContentScript()) { + return Promise.resolve([]); + } + + // I assume calling maybeLoadSourceMap is no longer needed, source maps are loaded lazily when referenced + // Bindings.debuggerWorkspaceBinding.maybeLoadSourceMap(script); + return ensureSourceMapLoadedAsync(script).then(/** @suppressGlobalPropertiesCheck */sourceMap => { + const scriptUrl = script.contentURL(); + const promises = []; + let realNamespace = false; + if (sourceMap) { + for (const url of sourceMap.sourceURLs()) { + // take only .cljs or .cljc urls, make sure url params and fragments get matched properly + // examples: + // http://localhost:9977/.compiled/demo/clojure/browser/event.cljs?rel=1463085025939 + // http://localhost:9977/.compiled/demo/dirac_sample/demo.cljs?rel=1463085026941 + const parser = document.createElement('a'); + parser.href = url; + if (parser.pathname.match(/\.clj.$/)) { + const contentProvider = sourceMap.sourceContentProvider(url, Common.resourceTypes.SourceMapScript); + const namespaceDescriptorsPromise = contentProvider.requestContent().then(cljsSourceCode => parseClojureScriptNamespaces(scriptUrl, cljsSourceCode.content)); + promises.push(namespaceDescriptorsPromise); + realNamespace = true; + } + } + } + + // we are also interested in pseudo namespaces from google closure library + if (!realNamespace) { + const parser = document.createElement('a'); + parser.href = scriptUrl; + if (parser.pathname.match(/\.js$/)) { + const namespaceDescriptorsPromise = script.requestContent().then(jsSourceCode => parsePseudoNamespaces(scriptUrl, jsSourceCode.content)); + promises.push(namespaceDescriptorsPromise); + } + } + + const concatResults = results => { + return [].concat.apply([], results); + }; + + return Promise.all(promises).then(concatResults); + }); + } + + // --- namespace names -------------------------------------------------------------------------------------------------- + + function getMacroNamespaceNames(namespaces) { + let names = []; + for (const descriptor of Object.values(namespaces)) { + if (!descriptor.detectedMacroNamespaces) { + continue; + } + names = names.concat(descriptor.detectedMacroNamespaces); + } + return dirac.deduplicate(names); + } + + function getSourceCodeNamespaceDescriptorsAsync(uiSourceCode) { + if (!uiSourceCode) { + return Promise.resolve([]); + } + const script = getScriptFromSourceCode(uiSourceCode); + if (!script) { + return Promise.resolve([]); + } + // noinspection JSCheckFunctionSignatures + return parseNamespacesDescriptorsAsync(script); + } + + function prepareNamespacesFromDescriptors(namespaceDescriptors) { + const result = {}; + for (const descriptor of namespaceDescriptors) { + result[descriptor.name] = descriptor; + } + return result; + } + + function extractNamespacesAsyncWorker() { + const workspace = Workspace.workspace; + if (!workspace) { + console.error('unable to locate Workspace when extracting all ClojureScript namespace names'); + return Promise.resolve([]); + } + + const uiSourceCodes = getRelevantSourceCodes(workspace); + const promises = []; + if (dirac.DEBUG_CACHES) { + console.log('extractNamespacesAsyncWorker initial processing of ' + uiSourceCodes.length + ' source codes'); + } + for (const uiSourceCode of uiSourceCodes) { + const namespaceDescriptorsPromise = getSourceCodeNamespaceDescriptorsAsync(uiSourceCode); + promises.push(namespaceDescriptorsPromise); + } + + const concatResults = results => { + return [].concat.apply([], results); + }; + + return Promise.all(promises).then(concatResults); + } + + let extractNamespacesAsyncInFlightPromise = null; + + function extractNamespacesAsync() { + // extractNamespacesAsync can take some time parsing all namespaces + // it could happen that extractNamespacesAsync() is called multiple times from code-completion code + // here we cache in-flight promise to prevent that + if (extractNamespacesAsyncInFlightPromise) { + return extractNamespacesAsyncInFlightPromise; + } + + if (dirac.namespacesCache) { + return Promise.resolve(dirac.namespacesCache); + } + + dirac.namespacesCache = {}; + startListeningForWorkspaceChanges(); + + extractNamespacesAsyncInFlightPromise = extractNamespacesAsyncWorker().then(descriptors => { + const newDescriptors = prepareNamespacesFromDescriptors(descriptors); + const newDescriptorsCount = Object.keys(newDescriptors).length; + if (!dirac.namespacesCache) { + dirac.namespacesCache = {}; + } + Object.assign(dirac.namespacesCache, newDescriptors); + const allDescriptorsCount = Object.keys(dirac.namespacesCache).length; + if (dirac.DEBUG_CACHES) { + console.log('extractNamespacesAsync finished namespacesCache with ' + newDescriptorsCount + ' items ' + + '(' + allDescriptorsCount + ' in total)'); + } + dirac.reportNamespacesCacheMutation(); + return dirac.namespacesCache; + }); + + extractNamespacesAsyncInFlightPromise.then(result => extractNamespacesAsyncInFlightPromise = null); + return extractNamespacesAsyncInFlightPromise; + } + + function invalidateNamespacesCache() { + if (dirac.DEBUG_CACHES) { + console.log('invalidateNamespacesCache'); + } + dirac.namespacesCache = null; + } + + function extractSourceCodeNamespacesAsync(uiSourceCode) { + if (!isRelevantSourceCode(uiSourceCode)) { + return Promise.resolve({}); + } + + return getSourceCodeNamespaceDescriptorsAsync(uiSourceCode).then(prepareNamespacesFromDescriptors); + } + + function extractAndMergeSourceCodeNamespacesAsync(uiSourceCode) { + if (!isRelevantSourceCode(uiSourceCode)) { + console.warn('extractAndMergeSourceCodeNamespacesAsync called on irrelevant source code', uiSourceCode); + return; + } + + if (dirac.DEBUG_CACHES) { + console.log('extractAndMergeSourceCodeNamespacesAsync', uiSourceCode); + } + const jobs = [extractNamespacesAsync(), extractSourceCodeNamespacesAsync(uiSourceCode)]; + return Promise.all(jobs).then(([namespaces, result]) => { + const addedNamespaceNames = Object.keys(result); + if (addedNamespaceNames.length) { + Object.assign(namespaces, result); + if (dirac.DEBUG_CACHES) { + console.log('updated namespacesCache by merging ', addedNamespaceNames, + 'from', uiSourceCode.contentURL(), + ' => new namespaces count:', Object.keys(namespaces).length); + } + dirac.reportNamespacesCacheMutation(); + } + return result; + }); + } + + function removeNamespacesMatchingUrl(url) { + extractNamespacesAsync().then(namespaces => { + const removedNames = []; + for (const namespaceName of Object.keys(namespaces)) { + const descriptor = namespaces[namespaceName]; + if (descriptor.url === url) { + delete namespaces[namespaceName]; + removedNames.push(namespaceName); + } + } + + if (dirac.DEBUG_CACHES) { + console.log('removeNamespacesMatchingUrl removed ' + removedNames.length + ' namespaces for url: ' + url + + ' new namespaces count:' + Object.keys(namespaces).length); + } + }); + } + + // --- namespace symbols ------------------------------------------------------------------------------------------------ + + /** + * @param {!Array} uiSourceCodes + * @param {function(string)} urlMatcherFn + * @return {!Array} + */ + function findMatchingSourceCodes(uiSourceCodes, urlMatcherFn) { + const matching = []; + for (let i = 0; i < uiSourceCodes.length; i++) { + const uiSourceCode = uiSourceCodes[i]; + if (urlMatcherFn(uiSourceCode.url())) { + matching.push(uiSourceCode); + } + } + return matching; + } + + /** + * @param {!Array} names + * @param {string} namespaceName + * @return {!Array} + */ + function filterNamesForNamespace(names, namespaceName) { + const prefix = namespaceName + '/'; + const prefixLength = prefix.length; + + return names.filter(name => name.startsWith(prefix)).map(name => name.substring(prefixLength)); + } + + /** + * @param {!Workspace.UISourceCode} uiSourceCode + * @return {?SDK.Script} + */ + function getScriptFromSourceCode(uiSourceCode) { + const target = SDK.targetManager.mainTarget(); + if (!target) { + throw new Error( + 'getScriptFromSourceCode called when there is no main target\n' + + `uiSourceCode: name=${uiSourceCode.name()} url=${uiSourceCode.url()} project=${uiSourceCode.project().type()}\n`); + } + const debuggerModel = /** @type {!SDK.DebuggerModel} */ (target.model(SDK.DebuggerModel)); + if (!debuggerModel) { + throw new Error( + `getScriptFromSourceCode called when main target has no debuggerModel target=${target}\n` + + `uiSourceCode: name=${uiSourceCode.name()} url=${uiSourceCode.url()} project=${uiSourceCode.project().type()}\n`); + } + const scriptFile = Bindings.debuggerWorkspaceBinding.scriptFile(uiSourceCode, debuggerModel); + if (!scriptFile) { + // do not treat missing script file as a fatal error, only log error into internal dirac console + // see https://github.com/binaryage/dirac/issues/79 + + // disabled to prevent console spam + if (dirac.DEBUG_CACHES) { + console.error( + 'uiSourceCode expected to have scriptFile associated\n' + + `uiSourceCode: name=${uiSourceCode.name()} url=${uiSourceCode.url()} project=${uiSourceCode.project().type()}\n`); + } + return null; + } + const script = scriptFile.getScript(); + if (!script) { + throw new Error( + 'uiSourceCode expected to have _script associated\n' + + `uiSourceCode: name=${uiSourceCode.name()} url=${uiSourceCode.url()} project=${uiSourceCode.project().type()}\n`); + } + if (!(script instanceof SDK.Script)) { + throw new Error( + 'getScriptFromSourceCode expected to return an instance of SDK.Script\n' + + `uiSourceCode: name=${uiSourceCode.name()} url=${uiSourceCode.url()} project=${uiSourceCode.project().type()}\n`); + } + return script; + } + + function extractNamesFromSourceMap(uiSourceCode, namespaceName) { + const script = getScriptFromSourceCode(uiSourceCode); + if (!script) { + console.error("unable to locate script when extracting symbols for ClojureScript namespace '" + namespaceName + "'"); + return []; + } + const sourceMap = Bindings.debuggerWorkspaceBinding.sourceMapForScript(/** @type {!SDK.Script} */(script)); + if (!sourceMap) { + console.error("unable to locate sourceMap when extracting symbols for ClojureScript namespace '" + namespaceName + "'"); + return []; + } + const payload = sourceMap.payload(); + if (!payload) { + console.error("unable to locate payload when extracting symbols for ClojureScript namespace '" + namespaceName + "'"); + return []; + } + return payload.names || []; + } + + function extractNamespaceSymbolsAsyncWorker(namespaceName) { + const workspace = Workspace.workspace; + if (!workspace) { + console.error("unable to locate Workspace when extracting symbols for ClojureScript namespace '" + namespaceName + "'"); + return Promise.resolve([]); + } + + return new Promise(resolve => { + const urlMatcherFn = prepareUrlMatcher(namespaceName); + const uiSourceCodes = getRelevantSourceCodes(workspace); + + // not there may be multiple matching sources for given namespaceName + // figwheel reloading is just adding new files and not removing old ones + const matchingSourceCodes = findMatchingSourceCodes(uiSourceCodes, urlMatcherFn); + if (!matchingSourceCodes.length) { + if (dirac.DEBUG_CACHES) { + console.warn("cannot find any matching source file for ClojureScript namespace '" + namespaceName + "'"); + } + resolve([]); + return; + } + + // we simply extract names from all matching source maps and then we filter them to match our namespace name and + // deduplicate them + const results = []; + for (const uiSourceCode of matchingSourceCodes) { + results.push(extractNamesFromSourceMap(uiSourceCode, namespaceName)); + } + const allNames = [].concat.apply([], results); + const filteredNames = unique(filterNamesForNamespace(allNames, namespaceName)); + + if (dirac.DEBUG_CACHES) { + console.log('extracted ' + filteredNames.length + ' symbol names for namespace', namespaceName, matchingSourceCodes.map(i => i.url())); + } + + resolve(filteredNames); + }); + } + + function extractNamespaceSymbolsAsync(namespaceName) { + if (!namespaceName) { + return Promise.resolve([]); + } + + if (namespacesSymbolsCache.has(namespaceName)) { + return namespacesSymbolsCache.get(namespaceName); + } + + const promisedResult = extractNamespaceSymbolsAsyncWorker(namespaceName); + + namespacesSymbolsCache.set(namespaceName, promisedResult); + + startListeningForWorkspaceChanges(); + return promisedResult; + } + + function invalidateNamespaceSymbolsCache(namespaceName = null) { + if (dirac.DEBUG_CACHES) { + console.log('invalidateNamespaceSymbolsCache', namespaceName); + } + if (namespaceName) { + namespacesSymbolsCache.delete(namespaceName); + } else { + namespacesSymbolsCache.clear(); + } + } + + // --- macro namespaces symbols ----------------------------------------------------------------------------------------- + // + // a situation is a bit more tricky here + // we don't have source mapping to clojure land in case of macro .clj files (makes no sense) + // but thanks to our access to all existing (ns ...) forms in the project we can infer at least some information + // we can at least collect macro symbols referred to via :refer + + function extractMacroNamespaceSymbolsAsyncWorker(namespaceName) { + + const collectMacroSymbols = namespaceDescriptors => { + const symbols = []; + for (const descriptor of Object.values(namespaceDescriptors)) { + const refers = descriptor.macroRefers; + if (!refers) { + continue; + } + for (const symbol of Object.keys(refers)) { + const ns = refers[symbol]; + if (ns === namespaceName) { + symbols.push(symbol); + } + } + } + return dirac.deduplicate(symbols); + }; + + return dirac.extractNamespacesAsync().then(collectMacroSymbols); + } + + function extractMacroNamespaceSymbolsAsync(namespaceName) { + if (!namespaceName) { + return Promise.resolve([]); + } + + const promisedResult = extractMacroNamespaceSymbolsAsyncWorker(namespaceName); + + if (dirac.DEBUG_CACHES) { + promisedResult.then(result => { + console.log('extractMacroNamespaceSymbolsAsync resolved', namespaceName, result); + }); + } + + return promisedResult; + } + + // --- changes ---------------------------------------------------------------------------------------------------------- + // this is to reflect dynamically updated files e.g. by Figwheel + + let listeningForWorkspaceChanges = false; + + function invalidateNamespaceSymbolsMatchingUrl(url) { + for (const namespaceName of namespacesSymbolsCache.keys()) { + const matcherFn = prepareUrlMatcher(namespaceName); + if (matcherFn(url)) { + dirac.invalidateNamespaceSymbolsCache(namespaceName); + } + } + } + + function handleSourceCodeAdded(event) { + const uiSourceCode = event.data; + if (uiSourceCode && isRelevantSourceCode(uiSourceCode)) { + const url = uiSourceCode.url(); + if (dirac.DEBUG_WATCHING) { + console.log('handleSourceCodeAdded', url); + } + extractAndMergeSourceCodeNamespacesAsync(uiSourceCode); + invalidateNamespaceSymbolsMatchingUrl(url); + } + } + + function handleSourceCodeRemoved(event) { + const uiSourceCode = event.data; + if (uiSourceCode && isRelevantSourceCode(uiSourceCode)) { + const url = uiSourceCode.url(); + if (dirac.DEBUG_WATCHING) { + console.log('handleSourceCodeRemoved', url); + } + removeNamespacesMatchingUrl(url); + invalidateNamespaceSymbolsMatchingUrl(url); + } + } + + function startListeningForWorkspaceChanges() { + if (listeningForWorkspaceChanges) { + return; + } + + if (dirac.DEBUG_WATCHING) { + console.log('startListeningForWorkspaceChanges'); + } + + const workspace = Workspace.workspace; + if (!workspace) { + console.error('unable to locate Workspace in startListeningForWorkspaceChanges'); + return; + } + + workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, handleSourceCodeAdded, dirac); + workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, handleSourceCodeRemoved, dirac); + + listeningForWorkspaceChanges = true; + } + + function stopListeningForWorkspaceChanges() { + if (!listeningForWorkspaceChanges) { + return; + } + + if (dirac.DEBUG_WATCHING) { + console.log('stopListeningForWorkspaceChanges'); + } + + const workspace = Workspace.workspace; + if (!workspace) { + console.error('unable to locate Workspace in stopListeningForWorkspaceChanges'); + return; + } + + workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeAdded, handleSourceCodeAdded, dirac); + workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, handleSourceCodeRemoved, dirac); + + listeningForWorkspaceChanges = false; + } + + function registerDiracLinkAction(action) { + if (Components.Linkifier.diracLinkHandlerAction) { + throw new Error('registerDiracLinkAction already set'); + } + Components.Linkifier.diracLinkHandlerAction = action; + } + + // --- exported interface ----------------------------------------------------------------------------------------------- + + // don't forget to update externs.js too + return { + lazyLoaded: true, + namespacesSymbolsCache: namespacesSymbolsCache, + namespacesCache: null, + REMOTE_OBJECT_PROPERTIES_FETCH_TIMEOUT: 1000, + lookupCurrentContext: lookupCurrentContext, + evalInCurrentContext: evalInCurrentContext, + hasCurrentContext: hasCurrentContext, + evalInDefaultContext: evalInDefaultContext, + hasDefaultContext: hasDefaultContext, + getMainDebuggerModel: getMainDebuggerModel, + subscribeDebuggerEvents: subscribeDebuggerEvents, + unsubscribeDebuggerEvents: unsubscribeDebuggerEvents, + addConsoleMessageToMainTarget: addConsoleMessageToMainTarget, + evaluateCommandInConsole: evaluateCommandInConsole, + startListeningForWorkspaceChanges: startListeningForWorkspaceChanges, + stopListeningForWorkspaceChanges: stopListeningForWorkspaceChanges, + extractScopeInfoFromScopeChainAsync: extractScopeInfoFromScopeChainAsync, + extractNamespaceSymbolsAsync: extractNamespaceSymbolsAsync, + invalidateNamespaceSymbolsCache: invalidateNamespaceSymbolsCache, + extractMacroNamespaceSymbolsAsync: extractMacroNamespaceSymbolsAsync, + extractNamespacesAsync: extractNamespacesAsync, + invalidateNamespacesCache: invalidateNamespacesCache, + getMacroNamespaceNames: getMacroNamespaceNames, + registerDiracLinkAction: registerDiracLinkAction + + }; + +})()); + +console.log('dirac-lazy module imported!'); diff --git a/front_end/dirac_lazy/module.json b/front_end/dirac_lazy/module.json new file mode 100644 index 0000000000..565450558d --- /dev/null +++ b/front_end/dirac_lazy/module.json @@ -0,0 +1,16 @@ +{ + "dependencies": [ + "sources", + "components", + "dirac" + ], + "modules": [ + "dirac_lazy.js" + ], + "scripts": [ + ], + "skip_compilation": [ + ], + "resources": [ + ] +} diff --git a/front_end/externs.js b/front_end/externs.js index 9c0a546248..d682659d23 100644 --- a/front_end/externs.js +++ b/front_end/externs.js @@ -322,6 +322,303 @@ diff_match_patch.prototype = { diff_cleanupSemantic(diff) {} }; +const dirac = { + /** @type {boolean} */ + DEBUG_EVAL: true, + /** @type {boolean} */ + hasFeature: true, + /** @type {boolean} */ + hasREPL: true, + /** @type {boolean} */ + hasParinfer: true, + /** @type {boolean} */ + hasFriendlyLocals: true, + /** @type {boolean} */ + hasClusteredLocals: true, + /** @type {boolean} */ + hasInlineCFs: true, + /** @type {boolean} */ + hasWelcomeMessage: true, + /** @type {boolean} */ + hasCleanUrls: true, + /** @type {boolean} */ + hasBeautifyFunctionNames: true, + /** @type {boolean} */ + hasLinkActions: true, + /** @type {?Object.} */ + namespacesCache: null, + + /** + * @param {string} name + * @return {boolean} + */ + getToggle: function (name) {}, + + /** + * @param {string} name + * @param {*} value + */ + setToggle: function (name, value) {}, + /** + * @return {!Promise} + */ + getReadyPromise: function () {}, + /** + * @param {string} code + * @return {string} + */ + codeAsString: function(code) {}, + /** + * @param {string} string + * @return {string} + */ + stringEscape: function(string) {}, + /** + * @param {string} action + */ + dispatchEventsForAction: function(action) {}, + /** + * @param {Node} node + * @param {string} query + */ + querySelectionAllDeep: function(node, query) {}, + lookupCurrentContext: function() {}, + /** + * @param {string} code + * @param {boolean} silent + * @param {?} callback + */ + evalInCurrentContext: function(code, silent, callback) {}, + /** + * @param {string} code + * @param {boolean} silent + * @param {?} callback + */ + evalInDefaultContext: function(code, silent, callback) {}, + /** + * @return {boolean} + */ + hasCurrentContext: function() {}, + /** + * @return {boolean} + */ + hasDefaultContext: function() {}, + + /** + * @return {?} + */ + getMainDebuggerModel: function() {}, + /** + * @param {?} callback + * @return {boolean} + * @this {Object} + */ + subscribeDebuggerEvents: function(callback) {}, + /** + * @param {?} callback + * @return {boolean} + */ + unsubscribeDebuggerEvents: function(callback) {}, + + /** + * @param {?} callFrame + * @return {!Promise} + */ + extractScopeInfoFromScopeChainAsync: function(callFrame) {}, + /** + * @param {string} namespaceName + * @return {!Promise>} + */ + extractNamespaceSymbolsAsync: function(namespaceName) {}, + /** + * @param {string} namespaceName + * @return {!Promise>} + */ + extractMacroNamespaceSymbolsAsync: function(namespaceName) {}, + /** + * @return {!Promise>} + */ + extractNamespacesAsync: function() {}, + + startListeningForWorkspaceChanges: function() {}, + stopListeningForWorkspaceChanges: function() {}, + /** + * @param {string=} namespaceName + */ + invalidateNamespaceSymbolsCache: function(namespaceName) {}, + invalidateNamespacesCache: function() {}, + + /** + * @param {Object.} namespaces + * @return {Array.} + */ + getMacroNamespaceNames: function(namespaces) {}, + + /** + * @param {!Object} action + */ + registerDiracLinkAction: function(action) {}, + + /** + * @param {Array.} coll + * @param {function(T):string=} keyFn + * @return {Array.} + * @template T + */ + deduplicate: function(coll, keyFn) {}, + + /** + * @param {Array.} array + * @param {function(T, T):number} comparator + * @return {Array.} + * @template T + */ + stableSort: function(array, comparator) {}, + + /** + * @param {string=} namespaceName + * @return {?dirac.NamespaceDescriptor} + */ + getNamespace: function(namespaceName) {}, + + /** + * @param {string} type + * @param {string} level + * @param {string} text + * @param {Array.<*>=} parameters + */ + addConsoleMessageToMainTarget: function(type, level, text, parameters) {}, + + // -- these are dynamically added by dirac.implant namespace ------------------------------------------------------------ + + initConsole: function() {}, + initRepl: function() {}, + /** + * @param {string} panelId + */ + notifyPanelSwitch: function(panelId) {}, + notifyFrontendInitialized: function() {}, + getVersion: function() {}, + getRuntimeTag: function(f) {}, + /** + * @param {Element} textAreaElement + * @param {boolean} useParinfer + * @return {!CodeMirror} + */ + adoptPrompt: function(textAreaElement, useParinfer) {}, + /** + * @param {number} requestId + * @param {string} code + * @param {dirac.ScopeInfo} scopeInfo + */ + sendEvalRequest: function(requestId, code, scopeInfo) {}, + /** + * @param {string} ns + * @param {string} ext + * @return {string} + */ + nsToRelpath: function(ns, ext) {}, + + triggerInternalError: function() {}, + triggerInternalErrorInPromise: function() {}, + triggerInternalErrorAsErrorLog: function() {}, + /** + * @param {string} name + * @return {string} + */ + getFunctionName: function(name) {}, + + /** + * @param {string} name + * @return {string} + */ + getFullFunctionName: function(name) {}, + + /** + * @return {!Promise.>} + */ + getReplSpecialsAsync: function() {}, + + /** + * @param {string} source + * @return {?dirac.NamespaceDescriptor} + */ + parseNsFromSource: function(source) {}, + + /** + * @return {boolean} + * */ + isIntercomReady: function() { + }, + + reportNamespacesCacheMutation: function() {}, + + /** + * @param {string} text + */ + feedback: function(text) {} +}; + +/** + * @typedef {{name:!string, identifier:?string}} + */ +dirac.ScopeFrameProp; + +/** + * @typedef {{title:?string, props:?Array.}} + */ +dirac.ScopeFrame; + +/** + * @typedef {{frames:!Array.}} + */ +dirac.ScopeInfo; + +/** + * @typedef {{ + * name:!string, + * url:!string, + * pseudo:?boolean, + * namespaceAliases:?Object., + * namespaceRefers:?Object., + * macroNamespaceAliases:?Object., + * macroRefers:?Object., + * detectedMacroNamespaces:?Array. + * }} + */ +dirac.NamespaceDescriptor; + +const Keysim = {} + +/** @constructor */ +Keysim.Keyboard = function() {}; +Keysim.Keyboard.prototype = { + /** + * Fires the correct sequence of events on the given target as if the given + * action was undertaken by a human. + * + * @param {string} action e.g. "alt+shift+left" or "backspace" + * @param {Element} target + * @param {?function()} callback + */ + dispatchEventsForAction: function (action, target, callback) { + }, + + /** + * Fires the correct sequence of events on the given target as if the given + * input had been typed by a human. + * + * @param {string} input + * @param {Element} target + * @param {?function()} callback + */ + dispatchEventsForInput: function (input, target, callback) { + }, +}; + +/** @type {Keysim.Keyboard} */ +Keysim.Keyboard.US_ENGLISH; + /** @constructor */ const Doc = function() {}; Doc.prototype = { @@ -482,7 +779,7 @@ CodeMirror.prototype = { undo: function() {}, unlinkDoc: function(other) {} }; -/** @type {!{cursorDiv: Element, lineSpace: Element, gutters: Element}} */ +/** @type {!{cursorDiv: Element, lineDiv: Element, lineSpace: Element, gutters: Element}} */ CodeMirror.prototype.display; /** @type {!{devtoolsAccessibleName: string, mode: string, lineWrapping: boolean}} */ CodeMirror.prototype.options; @@ -496,6 +793,7 @@ CodeMirror.getMode = function(options, spec) {}; CodeMirror.overlayMode = function(mode1, mode2, squashSpans) {}; CodeMirror.defineMode = function(modeName, modeConstructor) {}; CodeMirror.startState = function(mode) {}; +CodeMirror.runMode = function(string, modespec, callback, options) {}; CodeMirror.copyState = function(mode, state) {}; CodeMirror.inputStyles = {}; CodeMirror.inputStyles.textarea = class { diff --git a/front_end/host/InspectorFrontendHost.js b/front_end/host/InspectorFrontendHost.js index aea2f5bb9b..ce679183bc 100644 --- a/front_end/host/InspectorFrontendHost.js +++ b/front_end/host/InspectorFrontendHost.js @@ -142,7 +142,29 @@ export class InspectorFrontendHostStub { * @suppressGlobalPropertiesCheck */ inspectedURLChanged(url) { - document.title = Common.UIString.UIString('DevTools - %s', url.replace(/^https?:\/\//, '')); + // @ts-ignore + const dirac = window["dirac"]; + if (!dirac.isIntercomReady()) { + // postpone this code, we use document.title for signalling of frontend loading completion, see inspector.js + const that = this; + setTimeout(function() { that.inspectedURLChanged(url); }, 500); + return; + } + + const version = dirac.getVersion(); + dirac.getRuntimeTag( + /** + * @suppressGlobalPropertiesCheck + * @param {string} tag + */ + function(tag) { + if (!tag) { + tag = '[no runtime] ' + url; + } + document.title = 'Dirac v' + version + ' <-> ' + tag; + }); + // this is just for a temporary display, we will update it when get_runtime_tag calls us back with full runtime info + document.title = 'Dirac v' + version + ' <-> ' + url; } /** diff --git a/front_end/inspector.js b/front_end/inspector.js index b1b6801e7b..870c087ff2 100644 --- a/front_end/inspector.js +++ b/front_end/inspector.js @@ -5,3 +5,7 @@ import './devtools_app.js'; import * as Startup from './startup/startup.js'; Startup.RuntimeInstantiator.startApplication('inspector'); + +// this is here to signal our extension that we are done with our work, +// cannot easily inject script myself: https://bugs.chromium.org/p/chromium/issues/detail?id=30756 +document.title = '#'; diff --git a/front_end/main/MainImpl.js b/front_end/main/MainImpl.js index e9b0a29671..9b90b17018 100644 --- a/front_end/main/MainImpl.js +++ b/front_end/main/MainImpl.js @@ -1,3 +1,4 @@ +// @ts-nocheck // Copyright 2020 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. @@ -111,6 +112,12 @@ export class MainImpl { */ _gotPreferences(prefs) { console.timeStamp('Main._gotPreferences'); + // for dirac testing + if (Root.Runtime.queryParam('reset_settings')) { + console.info('DIRAC TESTING: clear devtools settings because reset_settings is present in url params'); + window.localStorage.clear(); // also wipe-out local storage to prevent tests flakiness + prefs = {}; + } this._createSettings(prefs); this._createAppUI(); } @@ -233,6 +240,8 @@ export class MainImpl { * @suppressGlobalPropertiesCheck */ async _createAppUI() { + await dirac.getReadyPromise(); + MainImpl.time('Main._createAppUI'); self.UI.viewManager = UI.ViewManager.ViewManager.instance(); @@ -369,6 +378,7 @@ export class MainImpl { // Allow UI cycles to repaint prior to creating connection. setTimeout(this._initializeTarget.bind(this), 0); MainImpl.timeEnd('Main._showAppUI'); + dirac.feedback('devtools ready'); } async _initializeTarget() { @@ -411,6 +421,7 @@ export class MainImpl { } this._lateInitDonePromise = Promise.all(promises); MainImpl.timeEnd('Main._lateInitialization'); + dirac.notifyFrontendInitialized(); } /** diff --git a/front_end/main/module.json b/front_end/main/module.json index d7e28c3d8f..d5a5492670 100644 --- a/front_end/main/module.json +++ b/front_end/main/module.json @@ -542,6 +542,7 @@ "i18n", "platform", "sdk", + "dirac", "persistence" ], "modules": [ diff --git a/front_end/object_ui/ObjectPropertiesSection.js b/front_end/object_ui/ObjectPropertiesSection.js index 29f6c3c445..a03f5452b3 100644 --- a/front_end/object_ui/ObjectPropertiesSection.js +++ b/front_end/object_ui/ObjectPropertiesSection.js @@ -123,12 +123,52 @@ export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow return objectPropertiesSection; } + /** + * @return {number} + */ + static PropertyCluster(property) { + // we want normal nice names to go first + // then all generated variable names with double underscores + // then all null values + // then all undefined values + try { + const value = property.value; + if (!value) { + return 3; + } + if (value.type === 'undefined') { + return 3; + } + if (value.subtype === 'null') { + return 2; + } + const name = property.name; + if (name.indexOf('__') != -1) { + return 1; + } + return 0; + } catch (e) { + return 4; + } + } + /** * @param {!SDK.RemoteObject.RemoteObjectProperty} propertyA * @param {!SDK.RemoteObject.RemoteObjectProperty} propertyB * @return {number} */ static CompareProperties(propertyA, propertyB) { + if (dirac.hasClusteredLocals) { + const clusterA = ObjectUI.ObjectPropertiesSection.PropertyCluster(propertyA); + const clusterB = ObjectUI.ObjectPropertiesSection.PropertyCluster(propertyB); + + if (clusterA > clusterB) { + return 1; + } + if (clusterA < clusterB) { + return -1; + } + } const a = propertyA.name; const b = propertyB.name; if (a === '__proto__') { @@ -167,9 +207,19 @@ export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow /** * @param {?string} name * @param {boolean=} isPrivate + * @param {string=} friendlyName + * @param {string=} friendlyNameNum * @return {!Element} */ - static createNameElement(name, isPrivate) { + static createNameElement(name, isPrivate, friendlyName, friendlyNameNum) { + if (friendlyName) { + let numHtml = ''; + if (friendlyNameNum) { + numHtml = UI.Fragment.html`${friendlyNameNum}`; + } + return UI.Fragment.html`${friendlyName}${numHtml}`; + } + if (name === null) { return UI.Fragment.html``; } @@ -685,6 +735,24 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { treeNode.appendChild(treeElement); } + /** + * @param {string} name + * @return {?string} + */ + function getFriendlyName(name) { + const duIndex = name.indexOf('__'); + if (duIndex != -1) { + return name.substring(0, duIndex); + } + const suMatch = name.match(/(.*?)_\d+$/); + if (suMatch) { + return suMatch[1]; + } + return null; + } + + const friendlyNamesTable = {}; + let previousProperty = null; const tailProperties = []; let protoProperty = null; for (let i = 0; i < properties.length; ++i) { @@ -693,10 +761,32 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { if (!ObjectPropertiesSection._isDisplayableProperty(property, treeNode.property)) { continue; } + + if (dirac.hasClusteredLocals) { + property._cluster = ObjectUI.ObjectPropertiesSection.PropertyCluster(property); + if (previousProperty && property._cluster != previousProperty._cluster) { + property._afterClusterBoundary = true; + previousProperty._beforeClusterBoundary = true; + } + } + + if (dirac.hasFriendlyLocals) { + const friendlyName = getFriendlyName(property.name); + if (friendlyName) { + property._friendlyName = friendlyName; + let num = friendlyNamesTable[friendlyName]; + if (!num) {num = 0;} + num += 1; + property._friendlyNameNum = num; + friendlyNamesTable[friendlyName] = num; + } + } + if (property.name === '__proto__' && !property.isAccessorProperty()) { protoProperty = property; continue; } + previousProperty = property; if (property.isOwn && property.getter) { const getterProperty = @@ -961,7 +1051,7 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { } update() { - this.nameElement = ObjectPropertiesSection.createNameElement(this.property.name, this.property.private); + this.nameElement = ObjectPropertiesSection.createNameElement(this.property.name, this.property.private, this.property._friendlyName, this.property._friendlyNameNum); if (!this.property.enumerable) { this.nameElement.classList.add('object-properties-section-dimmed'); } @@ -990,6 +1080,16 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { this.valueElement.title = Common.UIString.UIString('No property getter'); } + if (this.property._cluster !== undefined) { + const clusterClass = 'cluster-' + this.property._cluster; + this.listItemElement.classList.add(clusterClass); + } + if (this.property._beforeClusterBoundary) { + this.listItemElement.classList.add('before-cluster-boundary'); + } + if (this.property._afterClusterBoundary) { + this.listItemElement.classList.add('after-cluster-boundary'); + } const valueText = this.valueElement.textContent; if (this.property.value && valueText && !this.property.wasThrown) { this.expandedValueElement = this._createExpandedValueElement(this.property.value); @@ -1216,7 +1316,6 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { } } - /** * @unrestricted */ diff --git a/front_end/object_ui/customPreviewComponent.css b/front_end/object_ui/customPreviewComponent.css index a177340a40..358d847826 100644 --- a/front_end/object_ui/customPreviewComponent.css +++ b/front_end/object_ui/customPreviewComponent.css @@ -9,6 +9,11 @@ flex-direction: column; } +.custom-expandable-section-header { + cursor: pointer; + -webkit-user-select: none; +} + .custom-expand-icon { user-select: none; opacity: 50%; diff --git a/front_end/object_ui/objectPropertiesSection.css b/front_end/object_ui/objectPropertiesSection.css index ce13a5f25d..7d7c2e6818 100644 --- a/front_end/object_ui/objectPropertiesSection.css +++ b/front_end/object_ui/objectPropertiesSection.css @@ -4,6 +4,20 @@ * found in the LICENSE file. */ +.object-properties-section .name { + color: rgb(136, 19, 145); + flex-shrink: 0; +} + +.object-properties-section .name.friendly-name .friendly-num { + color: #999; +} + +.object-properties-section-separator { + flex-shrink: 0; + padding-right: 5px; +} + .object-properties-section-dimmed { opacity: 60%; } @@ -17,6 +31,11 @@ .object-properties-section li { user-select: text; + align-items: flex-start; +} + +.object-properties-section li.after-cluster-boundary { + border-top: 1px dashed #eee; } .object-properties-section li::before { @@ -56,8 +75,9 @@ } .name-and-value { - overflow: hidden; - text-overflow: ellipsis; + /* see https://github.com/binaryage/cljs-devtools/issues/52 + overflow: hidden; + text-overflow: ellipsis;*/ line-height: 16px; } diff --git a/front_end/protocol_client/module.json b/front_end/protocol_client/module.json index 70162816fe..2a07871f91 100644 --- a/front_end/protocol_client/module.json +++ b/front_end/protocol_client/module.json @@ -1,6 +1,7 @@ { "dependencies": [ "common", + "dirac", "host" ], "modules": [ diff --git a/front_end/screencast/ScreencastApp.js b/front_end/screencast/ScreencastApp.js index 7e90609939..16f369bad5 100644 --- a/front_end/screencast/ScreencastApp.js +++ b/front_end/screencast/ScreencastApp.js @@ -18,7 +18,7 @@ let _appInstance; */ export class ScreencastApp { constructor() { - this._enabledSetting = Common.Settings.Settings.instance().createSetting('screencastEnabled', true); + this._enabledSetting = Common.Settings.Settings.instance().createSetting('screencastEnabled', false); this._toggleButton = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Toggle screencast'), 'largeicon-phone'); this._toggleButton.setToggled(this._enabledSetting.get()); this._toggleButton.setEnabled(false); diff --git a/front_end/sdk/Connections.js b/front_end/sdk/Connections.js index b592838b83..04024cf68b 100644 --- a/front_end/sdk/Connections.js +++ b/front_end/sdk/Connections.js @@ -367,7 +367,7 @@ export function _createMainConnection(websocketConnectionLost) { const wsParam = Root.Runtime.Runtime.queryParam('ws'); const wssParam = Root.Runtime.Runtime.queryParam('wss'); if (wsParam || wssParam) { - const ws = wsParam ? `ws://${wsParam}` : `wss://${wssParam}`; + const ws = wsParam ? `ws://${decodeURIComponent(wsParam)}` : `wss://${decodeURIComponent(/** @type {string} */(wssParam))}`; return new WebSocketConnection(ws, websocketConnectionLost); } if (Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode()) { diff --git a/front_end/sdk/ConsoleModel.js b/front_end/sdk/ConsoleModel.js index 1ee895727f..7f9ab754f4 100644 --- a/front_end/sdk/ConsoleModel.js +++ b/front_end/sdk/ConsoleModel.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* * Copyright (C) 2011 Google Inc. All rights reserved. * @@ -202,6 +203,14 @@ export class ConsoleModel extends Common.ObjectWrapper.ObjectWrapper { this._clearIfNecessary(); } + if (msg.parameters) { + const firstParam = msg.parameters[0]; + if (firstParam && firstParam.value === '~~$DIRAC-MSG$~~') { + this.dispatchEventToListeners(SDK.ConsoleModel.Events.DiracMessage, msg); + return; + } + } + this._messages.push(msg); const runtimeModel = msg.runtimeModel(); if (msg._exceptionId && runtimeModel) { @@ -477,6 +486,7 @@ export class ConsoleModel extends Common.ObjectWrapper.ObjectWrapper { /** @enum {symbol} */ export const Events = { ConsoleCleared: Symbol('ConsoleCleared'), + DiracMessage: Symbol('DiracMessage'), MessageAdded: Symbol('MessageAdded'), MessageUpdated: Symbol('MessageUpdated'), CommandEvaluated: Symbol('CommandEvaluated') @@ -523,6 +533,7 @@ export class ConsoleMessage { this.executionContextId = executionContextId || 0; this.scriptId = scriptId || null; this.workerId = workerId || null; + this.skipHistory = false; this.frameId = null; if (!this.executionContextId && this._runtimeModel) { @@ -766,6 +777,8 @@ export const MessageType = { Result: 'result', Profile: 'profile', ProfileEnd: 'profileEnd', + DiracCommand: 'diracCommand', + DiracMarkup: 'diracMarkup', Command: 'command', System: 'system', QueryObjectResult: 'queryObjectResult' diff --git a/front_end/sdk/DebuggerModel.js b/front_end/sdk/DebuggerModel.js index af9d175f9a..ace16a100e 100644 --- a/front_end/sdk/DebuggerModel.js +++ b/front_end/sdk/DebuggerModel.js @@ -301,7 +301,7 @@ export class DebuggerModel extends SDKModel { } _asyncStackTracesStateChanged() { - const maxAsyncStackChainDepth = 32; + const maxAsyncStackChainDepth = 256; const enabled = !Common.Settings.Settings.instance().moduleSetting('disableAsyncStackTraces').get() && this._debuggerEnabled; const maxDepth = enabled ? maxAsyncStackChainDepth : 0; diff --git a/front_end/sdk/RuntimeModel.js b/front_end/sdk/RuntimeModel.js index f8165cccae..922b0be0ce 100644 --- a/front_end/sdk/RuntimeModel.js +++ b/front_end/sdk/RuntimeModel.js @@ -63,6 +63,32 @@ export class RuntimeModel extends SDKModel { Common.Settings.Settings.instance() .moduleSetting('customFormatters') .addChangeListener(this._customFormattersStateChanged.bind(this)); + + // note dirac module is initialized at this point because sdk module (our module) depends on dirac + // these should match "feature toggles" in dirac.js, dirac[name] = enabled + const flagNames = [ + 'hasREPL', + 'hasParinfer', + 'hasFriendlyLocals', + 'hasClusteredLocals', + 'hasInlineCFs', + 'hasWelcomeMessage', + 'hasCleanUrls', + 'hasBeautifyFunctionNames', + 'hasLinkActions' + ]; + + for (const flagName of flagNames) { + if (dirac.hostedInExtension) { + // in hosted mode we receive flags via dirac_flags url param + // we pass them down to moduleSetting + self.Common.moduleSetting(flagName).set(dirac.getToggle(flagName)); + } else { + // in internal mode we simply use flags from moduleSetting + dirac.setToggle(flagName, self.Common.moduleSetting(flagName).get()); + } + self.Common.moduleSetting(flagName).addChangeListener(this._diracToggleChanged.bind(this, flagName)); + } } /** @@ -253,6 +279,14 @@ export class RuntimeModel extends SDKModel { this._agent.invoke_setCustomObjectFormatterEnabled({enabled}); } + /** + * @param {string} name + * @param {!Common.EventTarget.EventTargetEvent} event + */ + _diracToggleChanged(name, event) { + dirac.setToggle(name, event.data); + } + /** * @param {string} expression * @param {string} sourceURL diff --git a/front_end/sdk/SourceMap.js b/front_end/sdk/SourceMap.js index 53fdfdabf5..e817342f73 100644 --- a/front_end/sdk/SourceMap.js +++ b/front_end/sdk/SourceMap.js @@ -107,6 +107,13 @@ export class SourceMap { dispose() { } + + /** + * @return {?SourceMapV3} + */ + payload() { + return null; + } } // eslint-disable-next-line no-unused-vars @@ -215,6 +222,7 @@ export class TextSourceMap { */ constructor(compiledURL, sourceMappingURL, payload, initiator) { this._initiator = initiator; + this._payload = payload; /** @type {?SourceMapV3} */ this._json = payload; this._compiledURL = compiledURL; @@ -279,6 +287,14 @@ export class TextSourceMap { return this._sourceMappingURL; } + /** + * @override + * @return {?SourceMapV3} + */ + payload() { + return this._payload; + } + /** * @override * @return {!Array.} @@ -716,6 +732,14 @@ export class WasmSourceMap { return WasmSourceMap.FAKE_URL; } + /** + * @override + * @return {?SourceMapV3} + */ + payload() { + return null; + } + /** * @override * @return {!Array.} diff --git a/front_end/sdk/module.json b/front_end/sdk/module.json index dfeb981270..ab3922829a 100644 --- a/front_end/sdk/module.json +++ b/front_end/sdk/module.json @@ -4,14 +4,93 @@ "host", "platform", "protocol_client", + "dirac", "text_utils" ], "extensions": [ + { + "type": "setting", + "category": "Dirac", + "title": "Enable REPL", + "settingName": "hasREPL", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Enable Parinfer", + "settingName": "hasParinfer", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Enable friendly locals", + "settingName": "hasFriendlyLocals", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Enable locals sorting/clustering", + "settingName": "hasClusteredLocals", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Display inlined custom formatters on Sources Panel", + "settingName": "hasInlineCFs", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Show welcome message in Console", + "settingName": "hasWelcomeMessage", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Clear urls from Figwheel cache busters", + "settingName": "hasCleanUrls", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Beautify mangled function names", + "settingName": "hasBeautifyFunctionNames", + "settingType": "boolean", + "defaultValue": true + }, + { + "type": "setting", + "category": "Dirac", + "title": "Enable link actions", + "settingName": "hasLinkActions", + "settingType": "boolean", + "defaultValue": false + }, { "type": "setting", "settingName": "skipStackFramesPattern", "settingType": "regex", - "defaultValue": "" + "defaultValue": [ + {"pattern":"cljs/.*\\.cljs$"}, + {"pattern":"clojure/.*\\.cljs$"}, + {"pattern":"goog/.*\\.js$"}, + {"pattern":"devtools/.*\\.cljs$"}, + {"pattern":"dirac/runtime/.*\\.cljs$"} + ] }, { "type": "setting", @@ -505,7 +584,7 @@ "title": "Enable custom formatters", "settingName": "customFormatters", "settingType": "boolean", - "defaultValue": false + "defaultValue": true }, { "type": "setting", diff --git a/front_end/shell.json b/front_end/shell.json index 9af08f569b..813864485c 100644 --- a/front_end/shell.json +++ b/front_end/shell.json @@ -7,6 +7,7 @@ { "name": "console_counters", "type": "autostart" }, { "name": "dom_extension", "type": "autostart" }, { "name": "extensions", "type": "autostart" }, + { "name": "dirac", "type": "autostart" }, { "name": "host", "type": "autostart" }, { "name": "i18n", "type": "autostart" }, { "name": "main", "type": "autostart" }, @@ -21,6 +22,7 @@ { "name": "ui", "type": "autostart" }, { "name": "workspace", "type": "autostart" }, + { "name": "dirac_lazy" }, { "name": "changes" }, { "name": "client_variations" }, { "name": "cm_modes" }, diff --git a/front_end/source_frame/SourcesTextEditor.js b/front_end/source_frame/SourcesTextEditor.js index 83913fa966..10c6528757 100644 --- a/front_end/source_frame/SourcesTextEditor.js +++ b/front_end/source_frame/SourcesTextEditor.js @@ -36,6 +36,9 @@ export class SourcesTextEditor extends TextEditor.CodeMirrorTextEditor.CodeMirro this._delegate = delegate; + if (dirac.hasInlineCFs) { + this.codeMirror().on('update', this._update.bind(this)); + } this.codeMirror().on('cursorActivity', this._cursorActivity.bind(this)); this.codeMirror().on('gutterClick', this._gutterClick.bind(this)); this.codeMirror().on('scroll', this._scroll.bind(this)); @@ -576,6 +579,32 @@ export class SourcesTextEditor extends TextEditor.CodeMirrorTextEditor.CodeMirro this.setMimeType(this.mimeType()); } + _reverseZOrder(element, startIndex) { + if (!element) { + return; + } + const childNodes = element.childNodes; + if (!childNodes) { + return; + } + let zindex = startIndex + childNodes.length - 1; + for (let i = 0; i < childNodes.length; i++) { + const child = childNodes[i]; + if (child) { + child.style.zIndex = zindex; + } + zindex--; + } + } + + _update(codeMirror) { + const linesDiv = codeMirror.display.lineDiv; + // custom formatters can provide expandable decoration widgets, + // they expand below and overlay following lines + // for this to work nicely, we have to make sure that z-order of code mirror lines is descending + this._reverseZOrder(linesDiv, 1); + } + _updateCodeFolding() { if (Common.Settings.Settings.instance().moduleSetting('textEditorCodeFolding').get()) { this.installGutter('CodeMirror-foldgutter', false); diff --git a/front_end/sources/CallStackSidebarPane.js b/front_end/sources/CallStackSidebarPane.js index 8e73c78f64..9d69a10676 100644 --- a/front_end/sources/CallStackSidebarPane.js +++ b/front_end/sources/CallStackSidebarPane.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* * Copyright (C) 2008 Apple Inc. All Rights Reserved. * @@ -237,6 +238,11 @@ export class CallStackSidebarPane extends UI.View.SimpleView { const title = element.createChild('div', 'call-frame-item-title'); const titleElement = title.createChild('div', 'call-frame-title-text'); titleElement.textContent = item.title; + if (dirac.hasBeautifyFunctionNames) { + if (item.functionName) { + titleElement.title = dirac.getFullFunctionName(item.functionName); + } + } if (item.isAsyncHeader) { element.classList.add('async-header'); } else { @@ -527,7 +533,7 @@ export class Item { * @return {!Promise} */ static async createForDebuggerCallFrame(frame, locationPool, updateDelegate) { - const item = new Item(UI.UIUtils.beautifyFunctionName(frame.functionName), updateDelegate); + const item = new Item(UI.UIUtils.beautifyFunctionName(frame.functionName), updateDelegate, frame.functionName); await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().createCallFrameLiveLocation( frame.location(), item._update.bind(item), locationPool); return item; @@ -551,7 +557,7 @@ export class Item { const asyncFrameItems = []; const liveLocationPromises = []; for (const frame of frames) { - const item = new Item(UI.UIUtils.beautifyFunctionName(frame.functionName), update); + const item = new Item(UI.UIUtils.beautifyFunctionName(frame.functionName), update, frame.functionName); const rawLocation = debuggerModel ? debuggerModel.createRawLocationByScriptId(frame.scriptId, frame.lineNumber, frame.columnNumber) : null; @@ -597,10 +603,12 @@ export class Item { /** * @param {string} title * @param {function(!Item):void} updateDelegate + * @param {?string} functionName */ - constructor(title, updateDelegate) { + constructor(title, updateDelegate, functionName = null) { this.isBlackboxed = false; this.title = title; + this.functionName = functionName; this.linkText = ''; this.uiLocation = null; this.isAsyncHeader = false; diff --git a/front_end/sources/DebuggerPlugin.js b/front_end/sources/DebuggerPlugin.js index 35b0dcb64b..34b720fb27 100644 --- a/front_end/sources/DebuggerPlugin.js +++ b/front_end/sources/DebuggerPlugin.js @@ -1061,13 +1061,29 @@ export class DebuggerPlugin extends Plugin { return; } - const valuesMap = new Map(); + /** + * @param {string} name + * @param {number|string=} line + * @param {number|string=} column + * @return {string} + */ + function getLocationId(name, line, column) { + line = line || '?'; + column = column || '?'; + return `${name}@${line}:${column}`; + } + + const infoMap = new Map(); for (const property of properties) { - valuesMap.set(property.name, property.value); + const locationId = getLocationId(property.name, property.originalNameLineNumber, property.originalNameColumnNumber); + infoMap.set(locationId, { + name: property.name, + value: property.value + }); } /** @type {!Map.>} */ - const namesPerLine = new Map(); + const infoIdsPerLine = new Map(); let skipObjectProperty = false; const tokenizer = new TextEditor.CodeMirrorUtils.TokenizerFactory().createTokenizer('text/javascript'); tokenizer(this._textEditor.line(fromLine).substring(fromColumn), processToken.bind(this, fromLine)); @@ -1084,31 +1100,41 @@ export class DebuggerPlugin extends Plugin { * @this {DebuggerPlugin} */ function processToken(editorLineNumber, tokenValue, tokenType, column, newColumn) { - if (!skipObjectProperty && tokenType && this._isIdentifier(tokenType) && valuesMap.get(tokenValue)) { - let names = namesPerLine.get(editorLineNumber); - if (!names) { - names = new Set(); - namesPerLine.set(editorLineNumber, names); + if (!skipObjectProperty && tokenType && this._isIdentifier(tokenType)) { + let exists = true; + let tokenLocationId = getLocationId(tokenValue, editorLineNumber, column); + if (!infoMap.has(tokenLocationId)) { + tokenLocationId = getLocationId(tokenValue); // a case without source-maps + if (!infoMap.has(tokenLocationId)) { + exists = false; + } + } + if (exists) { + let ids = infoIdsPerLine.get(editorLineNumber); + if (!ids) { + ids = new Set(); + infoIdsPerLine.set(editorLineNumber, ids); + } + ids.add(tokenLocationId); } - names.add(tokenValue); } skipObjectProperty = tokenValue === '.'; } - this._textEditor.operation(this._renderDecorations.bind(this, valuesMap, namesPerLine, fromLine, toLine)); + this._textEditor.operation(this._renderDecorations.bind(this, infoMap, infoIdsPerLine, fromLine, toLine)); } /** - * @param {!Map.} valuesMap - * @param {!Map.>} namesPerLine + * @param {!Map.} infoMap + * @param {!Map.>} infoIdsPerLine * @param {number} fromLine * @param {number} toLine */ - _renderDecorations(valuesMap, namesPerLine, fromLine, toLine) { + _renderDecorations(infoMap, infoIdsPerLine, fromLine, toLine) { const formatter = new ObjectUI.RemoteObjectPreviewFormatter.RemoteObjectPreviewFormatter(); for (let i = fromLine; i < toLine; ++i) { - const names = namesPerLine.get(i); + const infoIds = infoIdsPerLine.get(i); const oldWidget = this._valueWidgets.get(i); - if (!names) { + if (!infoIds) { if (oldWidget) { this._valueWidgets.delete(i); this._textEditor.removeDecoration(oldWidget, i); @@ -1126,27 +1152,31 @@ export class DebuggerPlugin extends Plugin { widget.__nameToToken = new Map(); let renderedNameCount = 0; - for (const name of names) { + for (const infoId of infoIds) { if (renderedNameCount > 10) { break; } - if (namesPerLine.get(i - 1) && namesPerLine.get(i - 1).has(name)) { - continue; - } // Only render name once in the given continuous block. + if (infoIdsPerLine.get(i - 1) && infoIdsPerLine.get(i - 1).has(infoId)) { + continue; // Only render name once in the given continuous block. + } if (renderedNameCount) { UI.UIUtils.createTextChild(widget, ', '); } const nameValuePair = widget.createChild('span'); - widget.__nameToToken.set(name, nameValuePair); - UI.UIUtils.createTextChild(nameValuePair, name + ' = '); - const value = valuesMap.get(name); + widget.__nameToToken.set(infoId, nameValuePair); + const info = infoMap.get(infoId); + UI.UIUtils.createTextChild(nameValuePair, info.name + ' = '); + const value = info.value; const propertyCount = value.preview ? value.preview.properties.length : 0; const entryCount = value.preview && value.preview.entries ? value.preview.entries.length : 0; - if (value.preview && propertyCount + entryCount < 10) { + if (dirac.hasInlineCFs && value.customPreview()) { + const customValueEl = (new ObjectUI.CustomPreviewComponent.CustomPreviewComponent(value)).element; + nameValuePair.appendChild(customValueEl); + } else if (value.preview && propertyCount + entryCount < 10) { formatter.appendObjectPreview(nameValuePair, value.preview, false /* isEntry */); } else { const propertyValue = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.createPropertyValue( - value, /* wasThrown */ false, /* showPreview */ false); + value, /* wasThrown */ false, /* showPreview */ false); nameValuePair.appendChild(propertyValue.element); } ++renderedNameCount; @@ -1162,7 +1192,7 @@ export class DebuggerPlugin extends Plugin { widgetChanged = true; // value has changed, update it. UI.UIUtils.runCSSAnimationOnce( - /** @type {!Element} */ (widget.__nameToToken.get(name)), 'source-frame-value-update-highlight'); + /** @type {!Element} */ (widget.__nameToToken.get(name)), 'source-frame-value-update-highlight'); } } if (widgetChanged) { diff --git a/front_end/sources/SourceMapNamesResolver.js b/front_end/sources/SourceMapNamesResolver.js index 9f714629cd..a39b422abe 100644 --- a/front_end/sources/SourceMapNamesResolver.js +++ b/front_end/sources/SourceMapNamesResolver.js @@ -30,6 +30,34 @@ export class Identifier { } } +export class NameDescriptor { + /** + * @param {string} name + * @param {number|undefined} lineNumber + * @param {number|undefined} columnNumber + */ + constructor(name, lineNumber, columnNumber) { + this.name = name; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } +} + + +export class MappingRecord { + /** + * @param {!NameDescriptor} compiledNameDescriptor + * @param {!NameDescriptor} originalNameDescriptor + */ + constructor(compiledNameDescriptor, originalNameDescriptor) { + this.compiledNameDescriptor = compiledNameDescriptor; + this.originalNameDescriptor = originalNameDescriptor; + } +} + +export class Mapping extends Array { +} + /** * @param {!SDK.DebuggerModel.ScopeChainEntry} scope * @return {!Promise>} @@ -93,7 +121,7 @@ export const scopeIdentifiers = function(scope) { /** * @param {!SDK.DebuggerModel.ScopeChainEntry} scope - * @return {!Promise.>} + * @return {!Promise} */ export const resolveScope = function(scope) { let identifiersPromise = scope[cachedIdentifiersSymbol]; @@ -104,7 +132,7 @@ export const resolveScope = function(scope) { const script = scope.callFrame().script; const sourceMap = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().sourceMapForScript(script); if (!sourceMap) { - return Promise.resolve(new Map()); + return Promise.resolve(/** @type {!Mapping} */([])); } /** @type {!Map} */ @@ -115,49 +143,49 @@ export const resolveScope = function(scope) { /** * @param {!Array} identifiers - * @return {!Promise>} + * @return {!Promise} */ function onIdentifiers(identifiers) { - const namesMapping = new Map(); + const namesMapping = /** @type {!Mapping} */([]); + const missingIdentifiers = []; // Extract as much as possible from SourceMap. for (let i = 0; i < identifiers.length; ++i) { const id = identifiers[i]; const entry = sourceMap.findEntry(id.lineNumber, id.columnNumber); if (entry && entry.name) { - namesMapping.set(id.name, entry.name); + const compiled = new NameDescriptor(id.name, id.lineNumber, id.columnNumber); + const original = new NameDescriptor(entry.name, entry.sourceLineNumber, entry.sourceColumnNumber); + namesMapping.push(new MappingRecord(compiled, original)); + } else { + missingIdentifiers.push(id); } } // Resolve missing identifier names from sourcemap ranges. - const promises = []; - for (let i = 0; i < identifiers.length; ++i) { - const id = identifiers[i]; - if (namesMapping.has(id.name)) { - continue; - } - const promise = resolveSourceName(id).then(onSourceNameResolved.bind(null, namesMapping, id)); - promises.push(promise); - } + const promises = missingIdentifiers.map(id => { + return resolveSourceName(id).then( + (originalNameDescriptor) => onSourceNameResolved(namesMapping, id, originalNameDescriptor)) + }); return Promise.all(promises) - .then(() => Sources.SourceMapNamesResolver._scopeResolvedForTest()) .then(() => namesMapping); } /** - * @param {!Map} namesMapping + * @param {!Mapping} namesMapping * @param {!Identifier} id - * @param {?string} sourceName + * @param {?NameDescriptor} originalNameDescriptor */ - function onSourceNameResolved(namesMapping, id, sourceName) { - if (!sourceName) { + function onSourceNameResolved(namesMapping, id, originalNameDescriptor) { + if (!originalNameDescriptor) { return; } - namesMapping.set(id.name, sourceName); + const compiled = new NameDescriptor(id.name, id.lineNumber, id.columnNumber); + namesMapping.push(new MappingRecord(compiled, originalNameDescriptor)); } /** * @param {!Identifier} id - * @return {!Promise} + * @return {!Promise} */ function resolveSourceName(id) { const startEntry = sourceMap.findEntry(id.lineNumber, id.columnNumber); @@ -165,7 +193,7 @@ export const resolveScope = function(scope) { if (!startEntry || !endEntry || !startEntry.sourceURL || startEntry.sourceURL !== endEntry.sourceURL || !startEntry.sourceLineNumber || !startEntry.sourceColumnNumber || !endEntry.sourceLineNumber || !endEntry.sourceColumnNumber) { - return Promise.resolve(/** @type {?string} */ (null)); + return Promise.resolve(null); } const sourceTextRange = new TextUtils.TextRange.TextRange( startEntry.sourceLineNumber, startEntry.sourceColumnNumber, endEntry.sourceLineNumber, @@ -174,21 +202,23 @@ export const resolveScope = function(scope) { Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForSourceMapSourceURL( script.debuggerModel, startEntry.sourceURL, script.isContentScript()); if (!uiSourceCode) { - return Promise.resolve(/** @type {?string} */ (null)); + return Promise.resolve(null); } return uiSourceCode.requestContent().then(deferredContent => { const content = deferredContent.content; - return onSourceContent(sourceTextRange, content); + return onSourceContent(sourceTextRange, startEntry.sourceLineNumber || 1, startEntry.sourceColumnNumber || 1, content); }); } /** * @param {!TextUtils.TextRange.TextRange} sourceTextRange + * @param {number} line + * @param {number} column * @param {?string} content - * @return {?string} + * @return {?NameDescriptor} */ - function onSourceContent(sourceTextRange, content) { + function onSourceContent(sourceTextRange, line, column, content) { if (!content) { return null; } @@ -198,13 +228,16 @@ export const resolveScope = function(scope) { textCache.set(content, text); } const originalIdentifier = text.extract(sourceTextRange).trim(); - return /[a-zA-Z0-9_$]+/.test(originalIdentifier) ? originalIdentifier : null; + if (!/[a-zA-Z0-9_$]+/.test(originalIdentifier)) { + return null; + } + return new NameDescriptor(originalIdentifier, line, column); } }; /** * @param {!SDK.DebuggerModel.CallFrame} callFrame - * @return {!Promise.>} + * @return {!Promise} */ export const allVariablesInCallFrame = function(callFrame) { const cached = callFrame[cachedMapSymbol]; @@ -221,24 +254,58 @@ export const allVariablesInCallFrame = function(callFrame) { return Promise.all(promises).then(mergeVariables); /** - * @param {!Array>} nameMappings - * @return {!Map} + * @param {!Array} nameMappings + * @return {!Mapping} */ function mergeVariables(nameMappings) { - const reverseMapping = new Map(); - for (const map of nameMappings) { - for (const compiledName of map.keys()) { - const originalName = map.get(compiledName); - if (!reverseMapping.has(originalName)) { - reverseMapping.set(originalName, compiledName); - } - } - } - callFrame[cachedMapSymbol] = reverseMapping; - return reverseMapping; + const mapping = /** @type {!Mapping} */(Array.prototype.concat.apply([], nameMappings)); + callFrame[cachedMapSymbol] = mapping; + return mapping; } }; +/** + * @param {!Mapping} mapping + * @param {string} name + * @param {number} line + * @param {number} column + * @return {?MappingRecord} + */ +const lookupMappingRecordForOriginalName = function(mapping, name, line, column) { + const res = mapping.filter(value => { + const desc = value.originalNameDescriptor; + return desc.name === name && desc.lineNumber === line && desc.columnNumber === column; + }); + if (res.length !== 1) { + return null; + } + return res[0]; +}; + +/** + * @param {!Mapping} mapping + * @param {string} name + * @return {!Array} + */ +const collectMappingRecordsForOriginalName = function(mapping, name) { + return mapping.filter(value => { + const desc = value.originalNameDescriptor; + return desc.name === name; + }); +}; + +/** + * @param {!Mapping} mapping + * @param {string} name + * @return {!Array} + */ +const collectMappingRecordsForCompiledName = function(mapping, name) { + return mapping.filter(value => { + const desc = value.compiledNameDescriptor; + return desc.name === name; + }); +}; + /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {string} originalText @@ -265,12 +332,14 @@ export const resolveExpression = function( /** * @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel - * @param {!Map} reverseMapping + * @param {!Mapping} mapping * @return {!Promise} */ - function findCompiledName(debuggerModel, reverseMapping) { - if (reverseMapping.has(originalText)) { - return Promise.resolve(reverseMapping.get(originalText) || ''); + function findCompiledName(debuggerModel, mapping) { + const record = lookupMappingRecordForOriginalName(mapping, + originalText, lineNumber, startColumnNumber); + if (record) { + return Promise.resolve(record.compiledNameDescriptor.name); } return resolveExpressionAsync(debuggerModel, uiSourceCode, lineNumber, startColumnNumber, endColumnNumber); @@ -339,7 +408,7 @@ export const resolveExpressionAsync = */ export const resolveThisObject = function(callFrame) { if (!callFrame) { - return Promise.resolve(/** @type {?SDK.RemoteObject.RemoteObject} */ (null)); + return Promise.resolve(null); } if (!callFrame.scopeChain().length) { return Promise.resolve(callFrame.thisObject()); @@ -348,19 +417,19 @@ export const resolveThisObject = function(callFrame) { return resolveScope(callFrame.scopeChain()[0]).then(onScopeResolved); /** - * @param {!Map} namesMapping + * @param {!Mapping} namesMapping * @return {!Promise} */ function onScopeResolved(namesMapping) { - const thisMappings = namesMapping.inverse().get('this'); - if (!thisMappings || thisMappings.size !== 1) { + const thisRecords = collectMappingRecordsForOriginalName(namesMapping, 'this'); + if (thisRecords.size !== 1) { return Promise.resolve(callFrame.thisObject()); } - const thisMapping = thisMappings.values().next().value; + const compiledName = thisRecords[0].compiledNameDescriptor.name; return callFrame .evaluate({ - expression: thisMapping, + expression: compiledName, objectGroup: 'backtrace', includeCommandLineAPI: false, silent: true, @@ -505,10 +574,23 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject { if (properties) { for (let i = 0; i < properties.length; ++i) { const property = properties[i]; - const name = namesMapping.get(property.name) || properties[i].name; - newProperties.push(new SDK.RemoteObject.RemoteObjectProperty( - name, property.value, property.enumerable, property.writable, property.isOwn, property.wasThrown, - property.symbol, property.synthetic)); + let name = property.name; + const propertyMapping = collectMappingRecordsForCompiledName(namesMapping, name); + if (propertyMapping.length > 0) { + // TODO: how to resolve the case when compiled name matches multiple original names? + // currently we don't have any information in property which would help us decide which one to take + name = propertyMapping[0].originalNameDescriptor.name; + } + const newProperty = new SDK.RemoteObject.RemoteObjectProperty( + name, property.value, property.enumerable, property.writable, property.isOwn, property.wasThrown, + property.symbol, property.synthetic); + if (propertyMapping.length > 0) { + // this is for _prepareScopeVariables, TODO: figure out a better way how to pass this info + newProperty.originalNameLineNumber = propertyMapping[0].originalNameDescriptor.lineNumber; + newProperty.originalNameColumnNumber = propertyMapping[0].originalNameDescriptor.columnNumber; + } + newProperties.push(newProperty); + newProperties[newProperties.length - 1].resolutionSourceProperty = property; } } return {properties: newProperties, internalProperties: internalProperties}; @@ -531,11 +613,10 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject { } let actualName = name; - for (const compiledName of namesMapping.keys()) { - if (namesMapping.get(compiledName) === name) { - actualName = compiledName; - break; - } + const matchingRecords = collectMappingRecordsForOriginalName(namesMapping, name); + if (matchingRecords.length > 0) { + // TODO: how to resolve the case when original name matches multiple compiled names? + actualName = matchingRecords[0].compiledNameDescriptor.name; } return this._object.setPropertyValue(actualName, value); } diff --git a/front_end/text_editor/cmdevtools.css b/front_end/text_editor/cmdevtools.css index 50ee2bc912..cf6730e1fc 100644 --- a/front_end/text_editor/cmdevtools.css +++ b/front_end/text_editor/cmdevtools.css @@ -567,17 +567,20 @@ div.CodeMirror:focus-within span.CodeMirror-nonmatchingbracket { .CodeMirror .text-editor-value-decoration { position: absolute; - bottom: 0; + top: -14px; /* we have to use top here for decoration widget to expand down */ white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + /* see https://github.com/binaryage/dirac/commit/eda4dc29a69dbdb4992639f3834025a92fe8be3c#commitcomment-35607061 + /* overflow: hidden; */ + /* text-overflow: ellipsis; */ max-width: 1000px; - opacity: 80%; + /* opacity: 80%; */ /* opacity does not play well with expandable widgets */ background-color: #ffe3c7; - margin-left: 10px; - padding-left: 5px; - color: #222; - user-select: text; + margin-left: 10px; + padding-left: 5px; + color: #222; + user-select: text; + border-radius: 2px; + cursor: default; } .CodeMirror .cm-execution-line .text-editor-value-decoration { @@ -605,6 +608,7 @@ div.CodeMirror:focus-within span.CodeMirror-nonmatchingbracket { .CodeMirror .CodeMirror-vscrollbar, .CodeMirror .CodeMirror-hscrollbar { transform: translateZ(0); + z-index: 2147483647; /* this is needed to appear above code lines, see _reverseZOrder */ } .cm-trailing-whitespace { diff --git a/front_end/third_party/codemirror/BUILD.gn b/front_end/third_party/codemirror/BUILD.gn index ba51a4f308..222d9c3ba1 100644 --- a/front_end/third_party/codemirror/BUILD.gn +++ b/front_end/third_party/codemirror/BUILD.gn @@ -31,6 +31,12 @@ devtools_pre_built("codemirror") { "package/addon/selection/active-line.js", "package/addon/selection/mark-selection.d.ts", "package/addon/selection/mark-selection.js", + # dirac - start + "package/addon/runmode/runmode.d.ts", + "package/addon/runmode/runmode.js", + "package/addon/display/placeholder.d.ts", + "package/addon/display/placeholder.js", + # dirac - end "package/lib/codemirror.d.ts", "package/lib/codemirror.js", "package/mode/clike/clike.d.ts", diff --git a/front_end/third_party/codemirror/package/addon/display/placeholder.d.ts b/front_end/third_party/codemirror/package/addon/display/placeholder.d.ts new file mode 100644 index 0000000000..693da49fc4 --- /dev/null +++ b/front_end/third_party/codemirror/package/addon/display/placeholder.d.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/front_end/third_party/codemirror/package/addon/runmode/runmode.d.ts b/front_end/third_party/codemirror/package/addon/runmode/runmode.d.ts new file mode 100644 index 0000000000..693da49fc4 --- /dev/null +++ b/front_end/third_party/codemirror/package/addon/runmode/runmode.d.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/front_end/ui/InspectorView.js b/front_end/ui/InspectorView.js index e1ed40f5c8..e92a7d3f98 100644 --- a/front_end/ui/InspectorView.js +++ b/front_end/ui/InspectorView.js @@ -272,6 +272,7 @@ export class InspectorView extends VBox { * @param {boolean} focus */ _showDrawer(focus) { + dirac.feedback('showDrawer'); if (this._drawerTabbedPane.isShowing()) { return; } @@ -379,6 +380,7 @@ export class InspectorView extends VBox { */ _tabSelected(event) { const tabId = /** @type {string} */ (event.data['tabId']); + dirac.notifyPanelSwitch(tabId); Host.userMetrics.panelShown(tabId); } diff --git a/front_end/ui/SuggestBox.js b/front_end/ui/SuggestBox.js index ee884fdcc5..5e0076a5f7 100644 --- a/front_end/ui/SuggestBox.js +++ b/front_end/ui/SuggestBox.js @@ -128,14 +128,16 @@ export class SuggestBox { * @return {number} */ _maxWidth(items) { - const kMaxWidth = 300; + const kMaxWidth = 100000; // dirac: do not limit max-width if (!items.length) { return kMaxWidth; } let maxItem; let maxLength = -Infinity; for (let i = 0; i < items.length; i++) { - const length = (items[i].title || items[i].text).length + (items[i].subtitle || '').length; + let length = (items[i].title || items[i].text).length + (items[i].subtitle || '').length; + const length2 = (items[i].epilogue || '').length; + length = 54 + 6.7 * length + 4.9 * length2; // dirac's suggestion items are more complex, this is a rough estimate if (length > maxLength) { maxLength = length; maxItem = items[i]; @@ -211,6 +213,9 @@ export class SuggestBox { const element = document.createElement('div'); element.classList.add('suggest-box-content-item'); element.classList.add('source-code'); + if (item.className) { + element.classList.add.apply(element.classList, item.className.split(' ')); + } if (item.iconType) { const icon = Icon.create(item.iconType, 'suggestion-icon'); element.appendChild(icon); @@ -219,18 +224,20 @@ export class SuggestBox { element.classList.add('secondary'); } element.tabIndex = -1; + element.createChild('span', 'prologue').textContent = (item.prologue || '').trimEndWithMaxLength(50); const maxTextLength = 50 + query.length; const displayText = (item.title || item.text).trim().trimEndWithMaxLength(maxTextLength).replace(/\n/g, '\u21B5'); const titleElement = element.createChild('span', 'suggestion-title'); const index = displayText.toLowerCase().indexOf(query.toLowerCase()); if (index > 0) { - titleElement.createChild('span').textContent = displayText.substring(0, index); + titleElement.createChild('span', 'pre-query').textContent = displayText.substring(0, index); } if (index > -1) { titleElement.createChild('span', 'query').textContent = displayText.substring(index, index + query.length); } - titleElement.createChild('span').textContent = displayText.substring(index > -1 ? index + query.length : 0); + titleElement.createChild('span', 'post-query').textContent = displayText.substring(index > -1 ? index + query.length : 0); + element.createChild('span', 'epilogue').textContent = (item.epilogue || '').trimEndWithMaxLength(50); titleElement.createChild('span', 'spacer'); if (item.subtitleRenderer) { const subtitleElement = /** @type {!HTMLElement} */ (item.subtitleRenderer.call(null)); @@ -410,6 +417,9 @@ export class SuggestBox { * selectionRange: ({startColumn: number, endColumn: number}|undefined), * hideGhostText: (boolean|undefined), * iconElement: (!HTMLElement|undefined), + * prologue?: (string|undefined), + * epilogue?: (string|undefined), + * className?: (string|undefined), * }} */ // @ts-ignore typedef diff --git a/front_end/ui/TextPrompt.js b/front_end/ui/TextPrompt.js index f9981805bf..9d967d34b3 100644 --- a/front_end/ui/TextPrompt.js +++ b/front_end/ui/TextPrompt.js @@ -136,7 +136,7 @@ export class TextPrompt extends Common.ObjectWrapper.ObjectWrapper { this._element.classList.add('text-prompt'); ARIAUtils.markAsTextBox(this._element); this._element.setAttribute('contenteditable', 'plaintext-only'); - this._element.addEventListener('keydown', this._boundOnKeyDown, false); + this._element.addEventListener('keydown', this._boundOnKeyDown, true); this._element.addEventListener('input', this._boundOnInput, false); this._element.addEventListener('mousewheel', this._boundOnMouseWheel, false); this._element.addEventListener('selectstart', this._boundClearAutocomplete, false); @@ -274,7 +274,7 @@ export class TextPrompt extends Common.ObjectWrapper.ObjectWrapper { _removeFromElement() { this.clearAutocomplete(); - this._element.removeEventListener('keydown', this._boundOnKeyDown, false); + this._element.removeEventListener('keydown', this._boundOnKeyDown, true); this._element.removeEventListener('input', this._boundOnInput, false); this._element.removeEventListener('selectstart', this._boundClearAutocomplete, false); this._element.removeEventListener('blur', this._boundClearAutocomplete, false); @@ -365,9 +365,10 @@ export class TextPrompt extends Common.ObjectWrapper.ObjectWrapper { } break; } - - if (isEnterKey(event)) { - event.preventDefault(); + if (!dirac.ignoreEnter) { + if (isEnterKey(event)) { + event.preventDefault(); + } } if (handled) { @@ -762,6 +763,24 @@ export class TextPrompt extends Common.ObjectWrapper.ObjectWrapper { selection.addRange(selectionRange); } + moveCaretToIndex(index) { + const selection = this._element.getComponentSelection(); + const selectionRange = this._createRange(); + + selectionRange.setStart(this._element.firstChild, index); + selectionRange.setEnd(this._element.firstChild, index); + + selection.removeAllRanges(); + selection.addRange(selectionRange); + } + + /** + * @return {string} + */ + getSuggestBoxRepresentation() { + return 'getSuggestBoxRepresentation not implemented for UI.TextPrompt'; + } + /** * @return {number} -1 if no caret can be found in text prompt */ diff --git a/front_end/ui/UIUtils.js b/front_end/ui/UIUtils.js index d3798da024..f71ee8ae7e 100644 --- a/front_end/ui/UIUtils.js +++ b/front_end/ui/UIUtils.js @@ -1212,6 +1212,9 @@ export function initializeUIUtils(document, themeSetting) { * @return {string} */ export function beautifyFunctionName(name) { + if (dirac.hasBeautifyFunctionNames) { + return dirac.getFunctionName(name); + } return name || Common.UIString.UIString('(anonymous)'); } diff --git a/front_end/ui/inspectorViewTabbedPane.css b/front_end/ui/inspectorViewTabbedPane.css index 077252beff..a5120bcf6a 100644 --- a/front_end/ui/inspectorViewTabbedPane.css +++ b/front_end/ui/inspectorViewTabbedPane.css @@ -21,6 +21,10 @@ margin-left: 0; } +.tabbed-pane-tab-slider { + -webkit-filter: hue-rotate(280deg); +} + .tabbed-pane-left-toolbar { margin-right: 0 !important; } diff --git a/front_end/ui/suggestBox.css b/front_end/ui/suggestBox.css index d337fd42e7..f125266fc7 100644 --- a/front_end/ui/suggestBox.css +++ b/front_end/ui/suggestBox.css @@ -129,3 +129,102 @@ color: HighlightText; } } + +/* dirac */ + +.suggest-box-content-item.suggest-cljs { + display: block; /* flex was causing troubles for cljs suggestion items, see _updateWidth */ +} + +.suggest-box-content-item.suggest-cljs .prologue::after { + display: inline-block; + font-size: 8px; + min-width: 42px; + content: ""; + -webkit-user-select: none; + position: relative; + margin-right: 6px; + color: #ccc; + text-align: right; +} + +.suggest-box-content-item.suggest-cljs .prologue::before { + height: 6px; + width: 0px; + display: inline-block; + border-left: 3px solid #aed17d; + border-right: 3px solid #aed17d; + border-radius: 1px; + content: ""; + -webkit-user-select: none; +} + +.suggest-box-content-item.suggest-cljs-macro .prologue::before { + border-left-color: #d1585d; + border-right-color: #d1585d; +} + +.suggest-box-content-item.suggest-cljs-pseudo .prologue::before { + border-left-color: #899fcb; + border-right-color: #899fcb; +} + +.suggest-box-content-item.suggest-cljs-special .prologue::before { + border-left-color: #e6bf73; + border-right-color: #e6bf73; +} + +.suggest-box-content-item.suggest-cljs-combined-ns-macro .prologue::before { + border-left-color: #d1585d; + border-right-color: #aed17d; +} + +.suggest-box-content-item.suggest-cljs-ns .prologue::after { + content: "ns"; +} + +.suggest-box-content-item.suggest-cljs-core .prologue::after { + content: "core"; +} + +.suggest-box-content-item.suggest-cljs-in-ns .prologue::after { + content: "in-ns"; +} + +.suggest-box-content-item.suggest-cljs-scope .prologue::after { + content: "scope"; +} + +.suggest-box-content-item.suggest-cljs-qualified .prologue::after { + content: "/"; +} + +.suggest-box-content-item.suggest-cljs-ns-alias .prologue::after { + content: "alias"; +} + +.suggest-box-content-item.suggest-cljs-refer .prologue::after { + content: "refer"; +} + +.suggest-box-content-item.suggest-cljs-repl .prologue::after { + content: "repl"; +} + +.suggest-box-content-item.suggest-cljs-aliased .prefix { + color: #ccc; +} + +.suggest-box-content-item.suggest-cljs-aliased .suffix { + color: #ccc; +} + +.suggest-box-content-item .epilogue { + font-size: 8px; + display:inline-block; + color: #ccc; + padding: 0 4px; + float: right; + position: relative; + top: 3px; +} diff --git a/front_end/ui/treeoutline.css b/front_end/ui/treeoutline.css index eedf8056ec..501bcbffdf 100644 --- a/front_end/ui/treeoutline.css +++ b/front_end/ui/treeoutline.css @@ -216,3 +216,26 @@ ol.tree-outline:not(.hide-selection-when-blurred) li.selected:focus * { color: HighlightText; } } + +/* dirac */ + +:host-context(.console-message) .tree-outline li { + min-height: 10px; /* min-height: 16px; was causing fat line in console if tree-outline was used */ +} + +/* do not apply padding to tree-outline when hosted in .console-message */ +:host-context(.console-message) { + padding: 0; +} + +:host-context(.console-message) .object-properties-section-root-element::before { + -webkit-mask-position: -4px -96px !important; +} + +:host-context(.console-message) .object-properties-section-root-element.expanded::before { + -webkit-mask-position: -20px -96px !important; +} + +.tree-outline { + overflow: auto; /* https://github.com/binaryage/dirac/issues/7 */ +} diff --git a/front_end/workspace/UISourceCode.js b/front_end/workspace/UISourceCode.js index 96fccbccd6..25a24ec206 100644 --- a/front_end/workspace/UISourceCode.js +++ b/front_end/workspace/UISourceCode.js @@ -150,6 +150,19 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper { } else { name = decodeURI(name); } + // @ts-ignore + if (dirac.hasCleanUrls) { + // strip all after ? in the name + const qmarkIndex = name.indexOf('?'); + if (qmarkIndex != -1) { + name = name.substring(0, qmarkIndex); + } + // strip all after # in the name + const hashIndex = name.indexOf('#'); + if (hashIndex != -1) { + name = name.substring(0, hashIndex); + } + } } catch (e) { } return skipTrim ? name : name.trimEndWithMaxLength(100); diff --git a/scripts/build/pdl.py b/scripts/build/pdl.py new file mode 100644 index 0000000000..d7733634e5 --- /dev/null +++ b/scripts/build/pdl.py @@ -0,0 +1,178 @@ +# Copyright 2018 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. + +from __future__ import print_function +import collections +import json +import os.path +import re +import sys + +description = '' + + +primitiveTypes = ['integer', 'number', 'boolean', 'string', 'object', + 'any', 'array', 'binary'] + + +def assignType(item, type, is_array=False, map_binary_to_string=False): + if is_array: + item['type'] = 'array' + item['items'] = collections.OrderedDict() + assignType(item['items'], type, False, map_binary_to_string) + return + + if type == 'enum': + type = 'string' + if map_binary_to_string and type == 'binary': + type = 'string' + if type in primitiveTypes: + item['type'] = type + else: + item['$ref'] = type + + +def createItem(d, experimental, deprecated, name=None): + result = collections.OrderedDict(d) + if name: + result['name'] = name + global description + if description: + result['description'] = description.strip() + if experimental: + result['experimental'] = True + if deprecated: + result['deprecated'] = True + return result + + +def parse(data, file_name, map_binary_to_string=False): + protocol = collections.OrderedDict() + protocol['version'] = collections.OrderedDict() + protocol['domains'] = [] + domain = None + item = None + subitems = None + nukeDescription = False + global description + lines = data.split('\n') + for i in range(0, len(lines)): + if nukeDescription: + description = '' + nukeDescription = False + line = lines[i] + trimLine = line.strip() + + if trimLine.startswith('#'): + if len(description): + description += '\n' + description += trimLine[2:] + continue + else: + nukeDescription = True + + if len(trimLine) == 0: + continue + + match = re.compile( + r'^(experimental )?(deprecated )?domain (.*)').match(line) + if match: + domain = createItem({'domain' : match.group(3)}, match.group(1), + match.group(2)) + protocol['domains'].append(domain) + continue + + match = re.compile(r'^ depends on ([^\s]+)').match(line) + if match: + if 'dependencies' not in domain: + domain['dependencies'] = [] + domain['dependencies'].append(match.group(1)) + continue + + match = re.compile(r'^ (experimental )?(deprecated )?type (.*) ' + r'extends (array of )?([^\s]+)').match(line) + if match: + if 'types' not in domain: + domain['types'] = [] + item = createItem({'id': match.group(3)}, match.group(1), match.group(2)) + assignType(item, match.group(5), match.group(4), map_binary_to_string) + domain['types'].append(item) + continue + + match = re.compile( + r'^ (experimental )?(deprecated )?(command|event) (.*)').match(line) + if match: + list = [] + if match.group(3) == 'command': + if 'commands' in domain: + list = domain['commands'] + else: + list = domain['commands'] = [] + else: + if 'events' in domain: + list = domain['events'] + else: + list = domain['events'] = [] + + item = createItem({}, match.group(1), match.group(2), match.group(4)) + list.append(item) + continue + + match = re.compile( + r'^ (experimental )?(deprecated )?(optional )?' + r'(array of )?([^\s]+) ([^\s]+)').match(line) + if match: + param = createItem({}, match.group(1), match.group(2), match.group(6)) + if match.group(3): + param['optional'] = True + assignType(param, match.group(5), match.group(4), map_binary_to_string) + if match.group(5) == 'enum': + enumliterals = param['enum'] = [] + subitems.append(param) + continue + + match = re.compile(r'^ (parameters|returns|properties)').match(line) + if match: + subitems = item[match.group(1)] = [] + continue + + match = re.compile(r'^ enum').match(line) + if match: + enumliterals = item['enum'] = [] + continue + + match = re.compile(r'^version').match(line) + if match: + continue + + match = re.compile(r'^ major (\d+)').match(line) + if match: + protocol['version']['major'] = match.group(1) + continue + + match = re.compile(r'^ minor (\d+)').match(line) + if match: + protocol['version']['minor'] = match.group(1) + continue + + match = re.compile(r'^ redirect ([^\s]+)').match(line) + if match: + item['redirect'] = match.group(1) + continue + + match = re.compile(r'^ ( )?[^\s]+$').match(line) + if match: + # enum literal + enumliterals.append(trimLine) + continue + + print('Error in %s:%s, illegal token: \t%s' % (file_name, i, line)) + sys.exit(1) + return protocol + + +def loads(data, file_name, map_binary_to_string=False): + if file_name.endswith(".pdl"): + return parse(data, file_name, map_binary_to_string) + return json.loads(data) diff --git a/scripts/check_gn.js b/scripts/check_gn.js index 6a95427c17..a3401ecb85 100644 --- a/scripts/check_gn.js +++ b/scripts/check_gn.js @@ -42,7 +42,10 @@ function checkNonAutostartNonRemoteModules() { } // e.g. "$resources_out_dir/lighthouse/lighthouse_module.js" => "lighthouse" - const mapLineToModuleName = line => line.split('/')[2].split('_module')[0]; + const mapLineToModuleName = line => { + const lineParts = line.split('/'); + return lineParts[lineParts.length - 1].split('_module')[0]; + }; const extraneousModules = lines.map(mapLineToModuleName).filter(module => !modules.includes(module)); if (extraneousModules.length) { diff --git a/scripts/closure/closure.iml b/scripts/closure/closure.iml new file mode 100644 index 0000000000..ebc7463016 --- /dev/null +++ b/scripts/closure/closure.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/jsdoc_validator/jsdoc_validator.iml b/scripts/jsdoc_validator/jsdoc_validator.iml new file mode 100644 index 0000000000..38d1f2791a --- /dev/null +++ b/scripts/jsdoc_validator/jsdoc_validator.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/migration/remove-unused-globals.sh b/scripts/migration/remove-unused-globals.sh index 70e7d995ad..8adae07d2f 100755 --- a/scripts/migration/remove-unused-globals.sh +++ b/scripts/migration/remove-unused-globals.sh @@ -1,12 +1,14 @@ -#!/bin/bash -script_full_path=$(dirname "$0") +#!/usr/bin/env bash -directories=$(find "$script_full_path/../../front_end/" -type d -maxdepth 1 -mindepth 1 -printf '%f\n') +set -e -o pipefail +scripts_migration_dir=$(dirname "${BASH_SOURCE[0]}") -cd $script_full_path +directories=$(find "$scripts_migration_dir/../../front_end/" -type d -maxdepth 1 -mindepth 1 -exec basename {} \;) + +cd "$scripts_migration_dir" npm run build for file in $directories; do - npm run remove-unused $file + npm run remove-unused "$file" done