UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

518 lines (470 loc) 17.8 kB
// Copyright (c) 2014 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. /** * @implements {UI.SuggestBoxDelegate} * @unrestricted */ TextEditor.TextEditorAutocompleteController = class { /** * @param {!TextEditor.CodeMirrorTextEditor} textEditor * @param {!CodeMirror} codeMirror * @param {!UI.AutocompleteConfig} config */ constructor(textEditor, codeMirror, config) { this._textEditor = textEditor; this._codeMirror = codeMirror; this._config = config; this._initialized = false; this._onScroll = this._onScroll.bind(this); this._onCursorActivity = this._onCursorActivity.bind(this); this._changes = this._changes.bind(this); this._blur = this._blur.bind(this); this._beforeChange = this._beforeChange.bind(this); this._mouseDown = () => { this.clearAutocomplete(); this._tooltipGlassPane.hide(); }; this._codeMirror.on('changes', this._changes); this._lastHintText = ''; /** @type {?UI.SuggestBox} */ this._suggestBox = null; /** @type {?string} */ this._currentSuggestion = null; this._hintElement = createElementWithClass('span', 'auto-complete-text'); this._tooltipGlassPane = new UI.GlassPane(); this._tooltipGlassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent); this._tooltipGlassPane.setOutsideClickCallback(this._tooltipGlassPane.hide.bind(this._tooltipGlassPane)); this._tooltipElement = createElementWithClass('div', 'autocomplete-tooltip'); const shadowRoot = UI.createShadowRootWithCoreStyles(this._tooltipGlassPane.contentElement, 'text_editor/autocompleteTooltip.css'); shadowRoot.appendChild(this._tooltipElement); } _initializeIfNeeded() { if (this._initialized) return; this._initialized = true; this._codeMirror.on('scroll', this._onScroll); this._codeMirror.on('cursorActivity', this._onCursorActivity); this._codeMirror.on('mousedown', this._mouseDown); this._codeMirror.on('blur', this._blur); if (this._config.isWordChar) { this._codeMirror.on('beforeChange', this._beforeChange); this._dictionary = new Common.TextDictionary(); this._addWordsFromText(this._codeMirror.getValue()); } } dispose() { this._codeMirror.off('changes', this._changes); if (this._initialized) { this._codeMirror.off('scroll', this._onScroll); this._codeMirror.off('cursorActivity', this._onCursorActivity); this._codeMirror.off('mousedown', this._mouseDown); this._codeMirror.off('blur', this._blur); } if (this._dictionary) { this._codeMirror.off('beforeChange', this._beforeChange); this._dictionary.reset(); } } /** * @param {!CodeMirror} codeMirror * @param {!CodeMirror.BeforeChangeObject} changeObject */ _beforeChange(codeMirror, changeObject) { this._updatedLines = this._updatedLines || {}; for (let i = changeObject.from.line; i <= changeObject.to.line; ++i) { if (this._updatedLines[i] === undefined) this._updatedLines[i] = this._codeMirror.getLine(i); } } /** * @param {string} text */ _addWordsFromText(text) { TextUtils.TextUtils.textToWords( text, /** @type {function(string):boolean} */ (this._config.isWordChar), addWord.bind(this)); /** * @param {string} word * @this {TextEditor.TextEditorAutocompleteController} */ function addWord(word) { if (word.length && (word[0] < '0' || word[0] > '9')) this._dictionary.addWord(word); } } /** * @param {string} text */ _removeWordsFromText(text) { TextUtils.TextUtils.textToWords( text, /** @type {function(string):boolean} */ (this._config.isWordChar), word => this._dictionary.removeWord(word)); } /** * @param {number} lineNumber * @param {number} columnNumber * @return {?TextUtils.TextRange} */ _substituteRange(lineNumber, columnNumber) { let range = this._config.substituteRangeCallback ? this._config.substituteRangeCallback(lineNumber, columnNumber) : null; if (!range && this._config.isWordChar) range = this._textEditor.wordRangeForCursorPosition(lineNumber, columnNumber, this._config.isWordChar); return range; } /** * @param {!TextUtils.TextRange} queryRange * @param {!TextUtils.TextRange} substituteRange * @param {boolean=} force * @return {!Promise.<!UI.SuggestBox.Suggestions>} */ _wordsWithQuery(queryRange, substituteRange, force) { const external = this._config.suggestionsCallback ? this._config.suggestionsCallback(queryRange, substituteRange, force) : null; if (external) return external; if (!this._dictionary || (!force && queryRange.isEmpty())) return Promise.resolve([]); let completions = this._dictionary.wordsWithPrefix(this._textEditor.text(queryRange)); const substituteWord = this._textEditor.text(substituteRange); if (this._dictionary.wordCount(substituteWord) === 1) completions = completions.filter(word => word !== substituteWord); completions.sort((a, b) => this._dictionary.wordCount(b) - this._dictionary.wordCount(a) || a.length - b.length); return Promise.resolve(completions.map(item => ({text: item}))); } /** * @param {!CodeMirror} codeMirror * @param {!Array.<!CodeMirror.ChangeObject>} changes */ _changes(codeMirror, changes) { if (!changes.length) return; if (this._dictionary && this._updatedLines) { for (const lineNumber in this._updatedLines) this._removeWordsFromText(this._updatedLines[lineNumber]); delete this._updatedLines; const linesToUpdate = {}; for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) { const changeObject = changes[changeIndex]; const editInfo = TextEditor.CodeMirrorUtils.changeObjectToEditOperation(changeObject); for (let i = editInfo.newRange.startLine; i <= editInfo.newRange.endLine; ++i) linesToUpdate[i] = this._codeMirror.getLine(i); } for (const lineNumber in linesToUpdate) this._addWordsFromText(linesToUpdate[lineNumber]); } let singleCharInput = false; let singleCharDelete = false; const cursor = this._codeMirror.getCursor('head'); for (let changeIndex = 0; changeIndex < changes.length; ++changeIndex) { const changeObject = changes[changeIndex]; if (changeObject.origin === '+input' && changeObject.text.length === 1 && changeObject.text[0].length === 1 && changeObject.to.line === cursor.line && changeObject.to.ch + 1 === cursor.ch) { singleCharInput = true; break; } if (changeObject.origin === '+delete' && changeObject.removed.length === 1 && changeObject.removed[0].length === 1 && changeObject.to.line === cursor.line && changeObject.to.ch - 1 === cursor.ch) { singleCharDelete = true; break; } } if (this._queryRange) { if (singleCharInput) this._queryRange.endColumn++; else if (singleCharDelete) this._queryRange.endColumn--; if (singleCharDelete || singleCharInput) this._setHint(this._lastHintText); } if (singleCharInput || singleCharDelete) setImmediate(this.autocomplete.bind(this)); else this.clearAutocomplete(); } _blur() { this.clearAutocomplete(); } /** * @param {!TextUtils.TextRange} mainSelection * @return {boolean} */ _validateSelectionsContexts(mainSelection) { const selections = this._codeMirror.listSelections(); if (selections.length <= 1) return true; const mainSelectionContext = this._textEditor.text(mainSelection); for (let i = 0; i < selections.length; ++i) { const wordRange = this._substituteRange(selections[i].head.line, selections[i].head.ch); if (!wordRange) return false; const context = this._textEditor.text(wordRange); if (context !== mainSelectionContext) return false; } return true; } /** * @param {boolean=} force */ autocomplete(force) { this._initializeIfNeeded(); if (this._codeMirror.somethingSelected()) { this._hideSuggestBox(); return; } const cursor = this._codeMirror.getCursor('head'); const substituteRange = this._substituteRange(cursor.line, cursor.ch); if (!substituteRange || !this._validateSelectionsContexts(substituteRange)) { this._hideSuggestBox(); return; } const queryRange = substituteRange.clone(); queryRange.endColumn = cursor.ch; const query = this._textEditor.text(queryRange); let hadSuggestBox = false; if (this._suggestBox) hadSuggestBox = true; this._wordsWithQuery(queryRange, substituteRange, force).then(wordsAcquired.bind(this)); /** * @param {!UI.SuggestBox.Suggestions} wordsWithQuery * @this {TextEditor.TextEditorAutocompleteController} */ function wordsAcquired(wordsWithQuery) { if (!wordsWithQuery.length || (wordsWithQuery.length === 1 && query === wordsWithQuery[0].text) || (!this._suggestBox && hadSuggestBox)) { this._hideSuggestBox(); this._onSuggestionsShownForTest([]); return; } if (!this._suggestBox) { this._suggestBox = new UI.SuggestBox(this, 20); if (this._config.anchorBehavior) this._suggestBox.setAnchorBehavior(this._config.anchorBehavior); } const oldQueryRange = this._queryRange; this._queryRange = queryRange; if (!oldQueryRange || queryRange.startLine !== oldQueryRange.startLine || queryRange.startColumn !== oldQueryRange.startColumn) this._updateAnchorBox(); this._suggestBox.updateSuggestions(this._anchorBox, wordsWithQuery, true, !this._isCursorAtEndOfLine(), query); if (this._suggestBox.visible) this._tooltipGlassPane.hide(); this._onSuggestionsShownForTest(wordsWithQuery); } } /** * @param {string} hint */ _setHint(hint) { const query = this._textEditor.text(this._queryRange); if (!hint || !this._isCursorAtEndOfLine() || !hint.startsWith(query)) { this._clearHint(); return; } const suffix = hint.substring(query.length).split('\n')[0]; this._hintElement.textContent = suffix; const cursor = this._codeMirror.getCursor('to'); if (this._hintMarker) { const position = this._hintMarker.position(); if (!position || !position.equal(TextUtils.TextRange.createFromLocation(cursor.line, cursor.ch))) { this._hintMarker.clear(); this._hintMarker = null; } } if (!this._hintMarker) { this._hintMarker = this._textEditor.addBookmark( cursor.line, cursor.ch, this._hintElement, TextEditor.TextEditorAutocompleteController.HintBookmark, true); } else if (this._lastHintText !== hint) { this._hintMarker.refresh(); } this._lastHintText = hint; } _clearHint() { if (!this._hintElement.textContent) return; this._lastHintText = ''; this._hintElement.textContent = ''; if (this._hintMarker) this._hintMarker.refresh(); } /** * @param {!UI.SuggestBox.Suggestions} suggestions */ _onSuggestionsShownForTest(suggestions) { } _onSuggestionsHiddenForTest() { } clearAutocomplete() { this._tooltipGlassPane.hide(); this._hideSuggestBox(); } _hideSuggestBox() { if (!this._suggestBox) return; this._suggestBox.hide(); this._suggestBox = null; this._queryRange = null; this._anchorBox = null; this._currentSuggestion = null; this._textEditor.dispatchEventToListeners(UI.TextEditor.Events.SuggestionChanged); this._clearHint(); this._onSuggestionsHiddenForTest(); } /** * @param {!KeyboardEvent} event * @return {boolean} */ keyDown(event) { if (this._tooltipGlassPane.isShowing() && event.keyCode === UI.KeyboardShortcut.Keys.Esc.code) { this._tooltipGlassPane.hide(); return true; } if (!this._suggestBox) return false; switch (event.keyCode) { case UI.KeyboardShortcut.Keys.Tab.code: this._suggestBox.acceptSuggestion(); this.clearAutocomplete(); return true; case UI.KeyboardShortcut.Keys.End.code: case UI.KeyboardShortcut.Keys.Right.code: if (this._isCursorAtEndOfLine()) { this._suggestBox.acceptSuggestion(); this.clearAutocomplete(); return true; } else { this.clearAutocomplete(); return false; } case UI.KeyboardShortcut.Keys.Left.code: case UI.KeyboardShortcut.Keys.Home.code: this.clearAutocomplete(); return false; case UI.KeyboardShortcut.Keys.Esc.code: this.clearAutocomplete(); return true; } return this._suggestBox.keyPressed(event); } /** * @return {boolean} */ _isCursorAtEndOfLine() { const cursor = this._codeMirror.getCursor('to'); return cursor.ch === this._codeMirror.getLine(cursor.line).length; } /** * @override * @param {string} suggestion * @param {boolean=} isIntermediateSuggestion */ applySuggestion(suggestion, isIntermediateSuggestion) { const oldSuggestion = this._currentSuggestion; this._currentSuggestion = suggestion; this._setHint(suggestion); if (oldSuggestion !== suggestion) this._textEditor.dispatchEventToListeners(UI.TextEditor.Events.SuggestionChanged); } /** * @override */ acceptSuggestion() { const selections = this._codeMirror.listSelections().slice(); const queryLength = this._queryRange.endColumn - this._queryRange.startColumn; const suggestion = this._currentSuggestion; this._codeMirror.operation(() => { for (let i = selections.length - 1; i >= 0; --i) { const start = selections[i].head; const end = new CodeMirror.Pos(start.line, start.ch - queryLength); this._codeMirror.replaceRange(suggestion, start, end, '+autocomplete'); } }); } /** * @return {string} */ textWithCurrentSuggestion() { if (!this._queryRange || this._currentSuggestion === null) return this._codeMirror.getValue(); const selections = this._codeMirror.listSelections().slice(); let last = {line: 0, column: 0}; let text = ''; const queryLength = this._queryRange.endColumn - this._queryRange.startColumn; for (const selection of selections) { const range = new TextUtils.TextRange(last.line, last.column, selection.head.line, selection.head.ch - queryLength); text += this._textEditor.text(range); text += this._currentSuggestion; last = {line: selection.head.line, column: selection.head.ch}; } const range = new TextUtils.TextRange(last.line, last.column, Infinity, Infinity); text += this._textEditor.text(range); return text; } _onScroll() { this._tooltipGlassPane.hide(); if (!this._suggestBox) 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.clearAutocomplete(); } else { this._updateAnchorBox(); this._suggestBox.setPosition(this._anchorBox); } } async _updateTooltip() { const cursor = this._codeMirror.getCursor(); const tooltip = this._config.tooltipCallback ? await this._config.tooltipCallback(cursor.line, cursor.ch) : null; const newCursor = this._codeMirror.getCursor(); if (newCursor.line !== cursor.line && newCursor.ch !== cursor.ch) return; if (this._suggestBox && this._suggestBox.visible) return; if (!tooltip) { this._tooltipGlassPane.hide(); return; } const metrics = this._textEditor.cursorPositionToCoordinates(cursor.line, cursor.ch); if (!metrics) { this._tooltipGlassPane.hide(); return; } this._tooltipGlassPane.setContentAnchorBox(new AnchorBox(metrics.x, metrics.y, 0, metrics.height)); this._tooltipElement.removeChildren(); this._tooltipElement.appendChild(tooltip); this._tooltipGlassPane.show(/** @type {!Document} */ (this._textEditor.element.ownerDocument)); } _onCursorActivity() { this._updateTooltip(); if (!this._suggestBox) return; const cursor = this._codeMirror.getCursor(); let shouldCloseAutocomplete = !(cursor.line === this._queryRange.startLine && this._queryRange.startColumn <= cursor.ch && cursor.ch <= this._queryRange.endColumn); // Try not to hide autocomplete when user types in. if (cursor.line === this._queryRange.startLine && cursor.ch === this._queryRange.endColumn + 1) { const line = this._codeMirror.getLine(cursor.line); shouldCloseAutocomplete = this._config.isWordChar ? !this._config.isWordChar(line.charAt(cursor.ch - 1)) : false; } if (shouldCloseAutocomplete) this.clearAutocomplete(); this._onCursorActivityHandledForTest(); } _onCursorActivityHandledForTest() { } _updateAnchorBox() { const line = this._queryRange.startLine; const column = this._queryRange.startColumn; const metrics = this._textEditor.cursorPositionToCoordinates(line, column); this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null; } }; TextEditor.TextEditorAutocompleteController.HintBookmark = Symbol('hint');