blob: 68352a10c223d6a6b027f2e0ab120a96dad4deae [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @implements {UI.Searchable}
* @implements {UI.Replaceable}
* @implements {SourceFrame.SourcesTextEditorDelegate}
* @unrestricted
*/
export class SourceFrameImpl extends UI.SimpleView {
/**
* @param {function(): !Promise<!Common.DeferredContent>} lazyContent
* @param {!UI.TextEditor.Options=} codeMirrorOptions
*/
constructor(lazyContent, codeMirrorOptions) {
super(Common.UIString('Source'));
this._lazyContent = lazyContent;
this._pretty = false;
/** @type {?string} */
this._rawContent = null;
/** @type {?Promise<{content: string, map: !Formatter.FormatterSourceMapping}>} */
this._formattedContentPromise = null;
/** @type {?Formatter.FormatterSourceMapping} */
this._formattedMap = null;
this._prettyToggle = new UI.ToolbarToggle(ls`Pretty print`, 'largeicon-pretty-print');
this._prettyToggle.addEventListener(UI.ToolbarButton.Events.Click, () => {
this._setPretty(!this._prettyToggle.toggled());
});
this._shouldAutoPrettyPrint = false;
this._prettyToggle.setVisible(false);
this._textEditor = new SourceFrame.SourcesTextEditor(this, codeMirrorOptions);
this._textEditor.show(this.element);
/** @type {?number} */
this._prettyCleanGeneration = null;
this._cleanGeneration = 0;
this._searchConfig = null;
this._delayedFindSearchMatches = null;
this._currentSearchResultIndex = -1;
this._searchResults = [];
this._searchRegex = null;
this._loadError = false;
this._textEditor.addEventListener(
SourceFrame.SourcesTextEditor.Events.EditorFocused, this._resetCurrentSearchResultIndex, this);
this._textEditor.addEventListener(
SourceFrame.SourcesTextEditor.Events.SelectionChanged, this._updateSourcePosition, this);
this._textEditor.addEventListener(UI.TextEditor.Events.TextChanged, event => {
if (!this._muteChangeEventsForSetContent) {
this.onTextChanged(event.data.oldRange, event.data.newRange);
}
});
/** @type {boolean} */
this._muteChangeEventsForSetContent = false;
this._sourcePosition = new UI.ToolbarText();
/**
* @type {?UI.SearchableView}
*/
this._searchableView = null;
this._editable = false;
this._textEditor.setReadOnly(true);
/** @type {?{line: number, column: (number|undefined), shouldHighlight: (boolean|undefined)}} */
this._positionToReveal = null;
this._lineToScrollTo = null;
this._selectionToSet = null;
this._loaded = false;
this._contentRequested = false;
this._highlighterType = '';
/** @type {!SourceFrame.Transformer} */
this._transformer = {
/**
* @param {number} editorLineNumber
* @param {number=} editorColumnNumber
* @return {!Array<number>}
*/
editorToRawLocation: (editorLineNumber, editorColumnNumber = 0) => {
if (!this._pretty) {
return [editorLineNumber, editorColumnNumber];
}
return this._prettyToRawLocation(editorLineNumber, editorColumnNumber);
},
/**
* @param {number} lineNumber
* @param {number=} columnNumber
* @return {!Array<number>}
*/
rawToEditorLocation: (lineNumber, columnNumber = 0) => {
if (!this._pretty) {
return [lineNumber, columnNumber];
}
return this._rawToPrettyLocation(lineNumber, columnNumber);
}
};
}
/**
* @param {boolean} canPrettyPrint
* @param {boolean=} autoPrettyPrint
*/
setCanPrettyPrint(canPrettyPrint, autoPrettyPrint) {
this._shouldAutoPrettyPrint = canPrettyPrint && !!autoPrettyPrint;
this._prettyToggle.setVisible(canPrettyPrint);
}
/**
* @param {boolean} value
* @return {!Promise}
*/
async _setPretty(value) {
this._pretty = value;
this._prettyToggle.setEnabled(false);
const wasLoaded = this.loaded;
const selection = this.selection();
let newSelection;
if (this._pretty) {
const formatInfo = await this._requestFormattedContent();
this._formattedMap = formatInfo.map;
this.setContent(formatInfo.content, null);
this._prettyCleanGeneration = this._textEditor.markClean();
const start = this._rawToPrettyLocation(selection.startLine, selection.startColumn);
const end = this._rawToPrettyLocation(selection.endLine, selection.endColumn);
newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]);
} else {
this.setContent(this._rawContent, null);
this._cleanGeneration = this._textEditor.markClean();
const start = this._prettyToRawLocation(selection.startLine, selection.startColumn);
const end = this._prettyToRawLocation(selection.endLine, selection.endColumn);
newSelection = new TextUtils.TextRange(start[0], start[1], end[0], end[1]);
}
if (wasLoaded) {
this.textEditor.revealPosition(newSelection.endLine, newSelection.endColumn, this._editable);
this.textEditor.setSelection(newSelection);
}
this._prettyToggle.setEnabled(true);
this._updatePrettyPrintState();
}
_updatePrettyPrintState() {
this._prettyToggle.setToggled(this._pretty);
this._textEditor.element.classList.toggle('pretty-printed', this._pretty);
if (this._pretty) {
this._textEditor.setLineNumberFormatter(lineNumber => {
const line = this._prettyToRawLocation(lineNumber - 1, 0)[0] + 1;
if (lineNumber === 1) {
return String(line);
}
if (line !== this._prettyToRawLocation(lineNumber - 2, 0)[0] + 1) {
return String(line);
}
return '-';
});
} else {
this._textEditor.setLineNumberFormatter(lineNumber => {
return String(lineNumber);
});
}
}
/**
* @return {!SourceFrame.Transformer}
*/
transformer() {
return this._transformer;
}
/**
* @param {number} line
* @param {number} column
* @return {!Array<number>}
*/
_prettyToRawLocation(line, column) {
if (!this._formattedMap) {
return [line, column];
}
return this._formattedMap.formattedToOriginal(line, column);
}
/**
* @param {number} line
* @param {number} column
* @return {!Array<number>}
*/
_rawToPrettyLocation(line, column) {
if (!this._formattedMap) {
return [line, column];
}
return this._formattedMap.originalToFormatted(line, column);
}
/**
* @param {boolean} editable
* @protected
*/
setEditable(editable) {
this._editable = editable;
if (this._loaded) {
this._textEditor.setReadOnly(!editable);
}
}
/**
* @return {boolean}
*/
hasLoadError() {
return this._loadError;
}
/**
* @override
*/
wasShown() {
this._ensureContentLoaded();
this._wasShownOrLoaded();
}
/**
* @override
*/
willHide() {
super.willHide();
this._clearPositionToReveal();
}
/**
* @override
* @return {!Array<!UI.ToolbarItem>}
*/
syncToolbarItems() {
return [this._prettyToggle, this._sourcePosition];
}
get loaded() {
return this._loaded;
}
get textEditor() {
return this._textEditor;
}
/**
* @protected
*/
get pretty() {
return this._pretty;
}
async _ensureContentLoaded() {
if (!this._contentRequested) {
this._contentRequested = true;
const {content, error} = (await this._lazyContent());
this._rawContent = error || content || '';
this._formattedContentPromise = null;
this._formattedMap = null;
this._prettyToggle.setEnabled(true);
if (error) {
this.setContent(null, error);
this._prettyToggle.setEnabled(false);
// Occasionally on load, there can be a race in which it appears the CodeMirror plugin
// runs the highlighter type assignment out of order. In case of an error then, set
// the highlighter type after a short delay. This appears to only occur the first
// time that CodeMirror is initialized, likely because the highlighter type was first
// initialized based on the file type, and the syntax highlighting is in a race
// with the new highlighter assignment. As the option is just an option and is not
// observable, we can't handle waiting for it here.
// https://github.com/codemirror/CodeMirror/issues/6019
// CRBug 1011445
setTimeout(() => this.setHighlighterType('text/plain'), 50);
} else {
if (this._shouldAutoPrettyPrint && TextUtils.isMinified(content || '')) {
await this._setPretty(true);
} else {
this.setContent(this._rawContent, null);
}
}
}
}
/**
* @return {!Promise<{content: string, map: !Formatter.FormatterSourceMapping}>}
*/
_requestFormattedContent() {
if (this._formattedContentPromise) {
return this._formattedContentPromise;
}
let fulfill;
this._formattedContentPromise = new Promise(x => fulfill = x);
new Formatter.ScriptFormatter(this._highlighterType, this._rawContent || '', (content, map) => {
fulfill({content, map});
});
return this._formattedContentPromise;
}
/**
* @param {number} line 0-based
* @param {number=} column
* @param {boolean=} shouldHighlight
*/
revealPosition(line, column, shouldHighlight) {
this._lineToScrollTo = null;
this._selectionToSet = null;
this._positionToReveal = {line: line, column: column, shouldHighlight: shouldHighlight};
this._innerRevealPositionIfNeeded();
}
_innerRevealPositionIfNeeded() {
if (!this._positionToReveal) {
return;
}
if (!this.loaded || !this.isShowing()) {
return;
}
const [line, column] =
this._transformer.rawToEditorLocation(this._positionToReveal.line, this._positionToReveal.column);
this._textEditor.revealPosition(line, column, this._positionToReveal.shouldHighlight);
this._positionToReveal = null;
}
_clearPositionToReveal() {
this._textEditor.clearPositionHighlight();
this._positionToReveal = null;
}
/**
* @param {number} line
*/
scrollToLine(line) {
this._clearPositionToReveal();
this._lineToScrollTo = line;
this._innerScrollToLineIfNeeded();
}
_innerScrollToLineIfNeeded() {
if (this._lineToScrollTo !== null) {
if (this.loaded && this.isShowing()) {
this._textEditor.scrollToLine(this._lineToScrollTo);
this._lineToScrollTo = null;
}
}
}
/**
* @return {!TextUtils.TextRange}
*/
selection() {
return this.textEditor.selection();
}
/**
* @param {!TextUtils.TextRange} textRange
*/
setSelection(textRange) {
this._selectionToSet = textRange;
this._innerSetSelectionIfNeeded();
}
_innerSetSelectionIfNeeded() {
if (this._selectionToSet && this.loaded && this.isShowing()) {
this._textEditor.setSelection(this._selectionToSet, true);
this._selectionToSet = null;
}
}
_wasShownOrLoaded() {
this._innerRevealPositionIfNeeded();
this._innerSetSelectionIfNeeded();
this._innerScrollToLineIfNeeded();
}
/**
* @param {!TextUtils.TextRange} oldRange
* @param {!TextUtils.TextRange} newRange
*/
onTextChanged(oldRange, newRange) {
const wasPretty = this.pretty;
this._pretty = this._prettyCleanGeneration !== null && this.textEditor.isClean(this._prettyCleanGeneration);
if (this._pretty !== wasPretty) {
this._updatePrettyPrintState();
}
this._prettyToggle.setEnabled(this.isClean());
if (this._searchConfig && this._searchableView) {
this.performSearch(this._searchConfig, false, false);
}
}
/**
* @return {boolean}
*/
isClean() {
return this.textEditor.isClean(this._cleanGeneration) ||
(this._prettyCleanGeneration !== null && this.textEditor.isClean(this._prettyCleanGeneration));
}
contentCommitted() {
this._cleanGeneration = this._textEditor.markClean();
this._prettyCleanGeneration = null;
this._rawContent = this.textEditor.text();
this._formattedMap = null;
this._formattedContentPromise = null;
if (this._pretty) {
this._pretty = false;
this._updatePrettyPrintState();
}
this._prettyToggle.setEnabled(true);
}
/**
* @param {string} content
* @param {string} mimeType
* @return {string}
*/
_simplifyMimeType(content, mimeType) {
if (!mimeType) {
return '';
}
// There are plenty of instances where TSX/JSX files are served with out the trailing x, i.e. JSX with a 'js' suffix
// which breaks the formatting. Therefore, if the mime type is TypeScript or JavaScript, we switch to the TSX/JSX
// superset so that we don't break formatting.
if (mimeType.indexOf('typescript') >= 0) {
return 'text/typescript-jsx';
}
if (mimeType.indexOf('javascript') >= 0 || mimeType.indexOf('jscript') >= 0 ||
mimeType.indexOf('ecmascript') >= 0) {
return 'text/jsx';
}
// A hack around the fact that files with "php" extension might be either standalone or html embedded php scripts.
if (mimeType === 'text/x-php' && content.match(/\<\?.*\?\>/g)) {
return 'application/x-httpd-php';
}
return mimeType;
}
/**
* @param {string} highlighterType
*/
setHighlighterType(highlighterType) {
this._highlighterType = highlighterType;
this._updateHighlighterType('');
}
/**
* @protected
* @return {string}
*/
highlighterType() {
return this._highlighterType;
}
/**
* @param {string} content
*/
_updateHighlighterType(content) {
this._textEditor.setMimeType(this._simplifyMimeType(content, this._highlighterType));
}
/**
* @param {?string} content
* @param {?string} loadError
*/
setContent(content, loadError) {
this._muteChangeEventsForSetContent = true;
if (!this._loaded) {
this._loaded = true;
if (!loadError) {
this._textEditor.setText(content || '');
this._cleanGeneration = this._textEditor.markClean();
this._textEditor.setReadOnly(!this._editable);
this._loadError = false;
} else {
this._textEditor.setText(loadError || '');
this._highlighterType = 'text/plain';
this._textEditor.setReadOnly(true);
this._loadError = true;
}
} else {
const scrollTop = this._textEditor.scrollTop();
const selection = this._textEditor.selection();
this._textEditor.setText(content || '');
this._textEditor.setScrollTop(scrollTop);
this._textEditor.setSelection(selection);
}
this._updateHighlighterType(content || '');
this._wasShownOrLoaded();
if (this._delayedFindSearchMatches) {
this._delayedFindSearchMatches();
this._delayedFindSearchMatches = null;
}
this._muteChangeEventsForSetContent = false;
}
/**
* @param {?UI.SearchableView} view
*/
setSearchableView(view) {
this._searchableView = view;
}
/**
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean} jumpBackwards
*/
_doFindSearchMatches(searchConfig, shouldJump, jumpBackwards) {
this._currentSearchResultIndex = -1;
this._searchResults = [];
const regex = searchConfig.toSearchRegex();
this._searchRegex = regex;
this._searchResults = this._collectRegexMatches(regex);
if (this._searchableView) {
this._searchableView.updateSearchMatchesCount(this._searchResults.length);
}
if (!this._searchResults.length) {
this._textEditor.cancelSearchResultsHighlight();
} else if (shouldJump && jumpBackwards) {
this.jumpToPreviousSearchResult();
} else if (shouldJump) {
this.jumpToNextSearchResult();
} else {
this._textEditor.highlightSearchResults(regex, null);
}
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
performSearch(searchConfig, shouldJump, jumpBackwards) {
if (this._searchableView) {
this._searchableView.updateSearchMatchesCount(0);
}
this._resetSearch();
this._searchConfig = searchConfig;
if (this.loaded) {
this._doFindSearchMatches(searchConfig, shouldJump, !!jumpBackwards);
} else {
this._delayedFindSearchMatches = this._doFindSearchMatches.bind(this, searchConfig, shouldJump, !!jumpBackwards);
}
this._ensureContentLoaded();
}
_resetCurrentSearchResultIndex() {
if (!this._searchResults.length) {
return;
}
this._currentSearchResultIndex = -1;
if (this._searchableView) {
this._searchableView.updateCurrentMatchIndex(this._currentSearchResultIndex);
}
this._textEditor.highlightSearchResults(/** @type {!RegExp} */ (this._searchRegex), null);
}
_resetSearch() {
this._searchConfig = null;
this._delayedFindSearchMatches = null;
this._currentSearchResultIndex = -1;
this._searchResults = [];
this._searchRegex = null;
}
/**
* @override
*/
searchCanceled() {
const range = this._currentSearchResultIndex !== -1 ? this._searchResults[this._currentSearchResultIndex] : null;
this._resetSearch();
if (!this.loaded) {
return;
}
this._textEditor.cancelSearchResultsHighlight();
if (range) {
this.setSelection(range);
}
}
jumpToLastSearchResult() {
this.jumpToSearchResult(this._searchResults.length - 1);
}
/**
* @return {number}
*/
_searchResultIndexForCurrentSelection() {
return this._searchResults.lowerBound(this._textEditor.selection().collapseToEnd(), TextUtils.TextRange.comparator);
}
/**
* @override
*/
jumpToNextSearchResult() {
const currentIndex = this._searchResultIndexForCurrentSelection();
const nextIndex = this._currentSearchResultIndex === -1 ? currentIndex : currentIndex + 1;
this.jumpToSearchResult(nextIndex);
}
/**
* @override
*/
jumpToPreviousSearchResult() {
const currentIndex = this._searchResultIndexForCurrentSelection();
this.jumpToSearchResult(currentIndex - 1);
}
/**
* @override
* @return {boolean}
*/
supportsCaseSensitiveSearch() {
return true;
}
/**
* @override
* @return {boolean}
*/
supportsRegexSearch() {
return true;
}
jumpToSearchResult(index) {
if (!this.loaded || !this._searchResults.length) {
return;
}
this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
if (this._searchableView) {
this._searchableView.updateCurrentMatchIndex(this._currentSearchResultIndex);
}
this._textEditor.highlightSearchResults(
/** @type {!RegExp} */ (this._searchRegex), this._searchResults[this._currentSearchResultIndex]);
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {string} replacement
*/
replaceSelectionWith(searchConfig, replacement) {
const range = this._searchResults[this._currentSearchResultIndex];
if (!range) {
return;
}
this._textEditor.highlightSearchResults(/** @type {!RegExp} */ (this._searchRegex), null);
const oldText = this._textEditor.text(range);
const regex = searchConfig.toSearchRegex();
let text;
if (regex.__fromRegExpQuery) {
text = oldText.replace(regex, replacement);
} else {
text = oldText.replace(regex, function() {
return replacement;
});
}
const newRange = this._textEditor.editRange(range, text);
this._textEditor.setSelection(newRange.collapseToEnd());
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {string} replacement
*/
replaceAllWith(searchConfig, replacement) {
this._resetCurrentSearchResultIndex();
let text = this._textEditor.text();
const range = this._textEditor.fullRange();
const regex = searchConfig.toSearchRegex(true);
if (regex.__fromRegExpQuery) {
text = text.replace(regex, replacement);
} else {
text = text.replace(regex, function() {
return replacement;
});
}
const ranges = this._collectRegexMatches(regex);
if (!ranges.length) {
return;
}
// Calculate the position of the end of the last range to be edited.
const currentRangeIndex = ranges.lowerBound(this._textEditor.selection(), TextUtils.TextRange.comparator);
const lastRangeIndex = mod(currentRangeIndex - 1, ranges.length);
const lastRange = ranges[lastRangeIndex];
const replacementLineEndings = replacement.computeLineEndings();
const replacementLineCount = replacementLineEndings.length;
const lastLineNumber = lastRange.startLine + replacementLineEndings.length - 1;
let lastColumnNumber = lastRange.startColumn;
if (replacementLineEndings.length > 1) {
lastColumnNumber =
replacementLineEndings[replacementLineCount - 1] - replacementLineEndings[replacementLineCount - 2] - 1;
}
this._textEditor.editRange(range, text);
this._textEditor.revealPosition(lastLineNumber, lastColumnNumber);
this._textEditor.setSelection(TextUtils.TextRange.createFromLocation(lastLineNumber, lastColumnNumber));
}
_collectRegexMatches(regexObject) {
const ranges = [];
for (let i = 0; i < this._textEditor.linesCount; ++i) {
let line = this._textEditor.line(i);
let offset = 0;
let match;
do {
match = regexObject.exec(line);
if (match) {
const matchEndIndex = match.index + Math.max(match[0].length, 1);
if (match[0].length) {
ranges.push(new TextUtils.TextRange(i, offset + match.index, i, offset + matchEndIndex));
}
offset += matchEndIndex;
line = line.substring(matchEndIndex);
}
} while (match && line);
}
return ranges;
}
/**
* @override
* @return {!Promise}
*/
populateLineGutterContextMenu(contextMenu, editorLineNumber) {
return Promise.resolve();
}
/**
* @override
* @return {!Promise}
*/
populateTextAreaContextMenu(contextMenu, editorLineNumber, editorColumnNumber) {
return Promise.resolve();
}
/**
* @return {boolean}
*/
canEditSource() {
return this._editable;
}
_updateSourcePosition() {
const selections = this._textEditor.selections();
if (!selections.length) {
return;
}
if (selections.length > 1) {
this._sourcePosition.setText(Common.UIString('%d selection regions', selections.length));
return;
}
let textRange = selections[0];
if (textRange.isEmpty()) {
const location = this._prettyToRawLocation(textRange.endLine, textRange.endColumn);
this._sourcePosition.setText(ls`Line ${location[0] + 1}, Column ${location[1] + 1}`);
return;
}
textRange = textRange.normalize();
const selectedText = this._textEditor.text(textRange);
if (textRange.startLine === textRange.endLine) {
this._sourcePosition.setText(Common.UIString('%d characters selected', selectedText.length));
} else {
this._sourcePosition.setText(Common.UIString(
'%d lines, %d characters selected', textRange.endLine - textRange.startLine + 1, selectedText.length));
}
}
}
/**
* @interface
*/
export class LineDecorator {
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {!TextEditor.CodeMirrorTextEditor} textEditor
* @param {string} type
*/
decorate(uiSourceCode, textEditor, type) {}
}
/* Legacy exported object */
self.SourceFrame = self.SourceFrame || {};
/* Legacy exported object */
SourceFrame = SourceFrame || {};
/** @constructor */
SourceFrame.SourceFrame = SourceFrameImpl;
/** @interface */
SourceFrame.LineDecorator = LineDecorator;
/**
* @typedef {{
* editorToRawLocation: function(number, number=):!Array<number>,
* rawToEditorLocation: function(number, number=):!Array<number>
* }}
*/
SourceFrame.Transformer;