blob: 4a9bbde7cc80ec49036e068cffc0a8e61e68aeaa [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 {Common.ContentProvider}
* @unrestricted
*/
export default class UISourceCode extends Common.Object {
/**
* @param {!Workspace.Project} project
* @param {string} url
* @param {!Common.ResourceType} contentType
*/
constructor(project, url, contentType) {
super();
this._project = project;
this._url = url;
const parsedURL = Common.ParsedURL.fromString(url);
if (parsedURL) {
this._origin = parsedURL.securityOrigin();
this._parentURL = this._origin + parsedURL.folderPathComponents;
this._name = parsedURL.lastPathComponent;
if (parsedURL.queryParams) {
this._name += '?' + parsedURL.queryParams;
}
} else {
this._origin = '';
this._parentURL = '';
this._name = url;
}
this._contentType = contentType;
/** @type {?Promise<!Common.DeferredContent>} */
this._requestContentPromise = null;
/** @type {?Platform.Multimap<string, !LineMarker>} */
this._decorations = null;
this._hasCommits = false;
/** @type {?Set<!Message>} */
this._messages = null;
this._contentLoaded = false;
/** @type {?Common.DeferredContent} */
this._content = null;
this._forceLoadOnCheckContent = false;
this._checkingContent = false;
/** @type {?string} */
this._lastAcceptedContent = null;
/** @type {?string} */
this._workingCopy = null;
/** @type {?function() : string} */
this._workingCopyGetter = null;
}
/**
* @return {!Promise<?UISourceCodeMetadata>}
*/
requestMetadata() {
return this._project.requestMetadata(this);
}
/**
* @return {string}
*/
name() {
return this._name;
}
/**
* @return {string}
*/
mimeType() {
return this._project.mimeType(this);
}
/**
* @return {string}
*/
url() {
return this._url;
}
/**
* @return {string}
*/
parentURL() {
return this._parentURL;
}
/**
* @return {string}
*/
origin() {
return this._origin;
}
/**
* @return {string}
*/
fullDisplayName() {
return this._project.fullDisplayName(this);
}
/**
* @param {boolean=} skipTrim
* @return {string}
*/
displayName(skipTrim) {
if (!this._name) {
return Common.UIString('(index)');
}
let name = this._name;
try {
if (this.project().type() === Workspace.projectTypes.FileSystem) {
name = unescape(name);
} else {
name = decodeURI(name);
}
} catch (e) {
}
return skipTrim ? name : name.trimEndWithMaxLength(100);
}
/**
* @return {boolean}
*/
canRename() {
return this._project.canRename();
}
/**
* @param {string} newName
* @return {!Promise<boolean>}
*/
rename(newName) {
let fulfill;
const promise = new Promise(x => fulfill = x);
this._project.rename(this, newName, innerCallback.bind(this));
return promise;
/**
* @param {boolean} success
* @param {string=} newName
* @param {string=} newURL
* @param {!Common.ResourceType=} newContentType
* @this {UISourceCode}
*/
function innerCallback(success, newName, newURL, newContentType) {
if (success) {
this._updateName(
/** @type {string} */ (newName), /** @type {string} */ (newURL),
/** @type {!Common.ResourceType} */ (newContentType));
}
fulfill(success);
}
}
remove() {
this._project.deleteFile(this);
}
/**
* @param {string} name
* @param {string} url
* @param {!Common.ResourceType=} contentType
*/
_updateName(name, url, contentType) {
const oldURL = this._url;
this._url = this._url.substring(0, this._url.length - this._name.length) + name;
this._name = name;
if (url) {
this._url = url;
}
if (contentType) {
this._contentType = contentType;
}
this.dispatchEventToListeners(Events.TitleChanged, this);
this.project().workspace().dispatchEventToListeners(
Workspace.Workspace.Events.UISourceCodeRenamed, {oldURL: oldURL, uiSourceCode: this});
}
/**
* @override
* @return {string}
*/
contentURL() {
return this.url();
}
/**
* @override
* @return {!Common.ResourceType}
*/
contentType() {
return this._contentType;
}
/**
* @override
* @return {!Promise<boolean>}
*/
async contentEncoded() {
await this.requestContent();
return this._contentEncoded || false;
}
/**
* @return {!Workspace.Project}
*/
project() {
return this._project;
}
/**
* @override
* @return {!Promise<!Common.DeferredContent>}
*/
requestContent() {
if (this._requestContentPromise) {
return this._requestContentPromise;
}
if (this._contentLoaded) {
return Promise.resolve(/** @type {!Common.DeferredContent} */ (this._content));
}
this._requestContentPromise = this._requestContentImpl();
return this._requestContentPromise;
}
/**
* @returns {!Promise<!UISourceCode>}
*/
async getFormatted() {
const formatData = await Sources.sourceFormatter.format(this);
return formatData.formattedSourceCode;
}
/**
* @returns {!Promise<!Common.DeferredContent>}
*/
async _requestContentImpl() {
try {
const content = await this._project.requestFileContent(this);
if (!this._contentLoaded) {
this._contentLoaded = true;
this._content = content;
this._contentEncoded = content.isEncoded;
}
} catch (err) {
this._contentLoaded = true;
this._content = {error: err ? String(err) : '', isEncoded: false};
}
return /** @type {!Common.DeferredContent} */ (this._content);
}
async checkContentUpdated() {
if (!this._contentLoaded && !this._forceLoadOnCheckContent) {
return;
}
if (!this._project.canSetFileContent() || this._checkingContent) {
return;
}
this._checkingContent = true;
const updatedContent = await this._project.requestFileContent(this);
this._checkingContent = false;
if (updatedContent.content === null) {
const workingCopy = this.workingCopy();
this._contentCommitted('', false);
this.setWorkingCopy(workingCopy);
return;
}
if (this._lastAcceptedContent === updatedContent.content) {
return;
}
if (this._content && this._content.content === updatedContent.content) {
this._lastAcceptedContent = null;
return;
}
if (!this.isDirty() || this._workingCopy === updatedContent.content) {
this._contentCommitted(/** @type {string} */ (updatedContent.content), false);
return;
}
await Common.Revealer.reveal(this);
// Make sure we are in the next frame before stopping the world with confirm
await new Promise(resolve => setTimeout(resolve, 0));
const shouldUpdate = window.confirm(ls`This file was changed externally. Would you like to reload it?`);
if (shouldUpdate) {
this._contentCommitted(/** @type {string} */ (updatedContent.content), false);
} else {
this._lastAcceptedContent = updatedContent.content;
}
}
forceLoadOnCheckContent() {
this._forceLoadOnCheckContent = true;
}
/**
* @param {string} content
*/
_commitContent(content) {
if (this._project.canSetFileContent()) {
this._project.setFileContent(this, content, false);
}
this._contentCommitted(content, true);
}
/**
* @param {string} content
* @param {boolean} committedByUser
*/
_contentCommitted(content, committedByUser) {
this._lastAcceptedContent = null;
this._content = {content, isEncoded: false};
this._contentLoaded = true;
this._requestContentPromise = null;
this._hasCommits = true;
this._innerResetWorkingCopy();
const data = {uiSourceCode: this, content, encoded: this._contentEncoded};
this.dispatchEventToListeners(Events.WorkingCopyCommitted, data);
this._project.workspace().dispatchEventToListeners(Workspace.Workspace.Events.WorkingCopyCommitted, data);
if (committedByUser) {
this._project.workspace().dispatchEventToListeners(Workspace.Workspace.Events.WorkingCopyCommittedByUser, data);
}
}
/**
* @param {string} content
*/
addRevision(content) {
this._commitContent(content);
}
/**
* @return {boolean}
*/
hasCommits() {
return this._hasCommits;
}
/**
* @return {string}
*/
workingCopy() {
if (this._workingCopyGetter) {
this._workingCopy = this._workingCopyGetter();
this._workingCopyGetter = null;
}
if (this.isDirty()) {
return /** @type {string} */ (this._workingCopy);
}
return (this._content && this._content.content) || '';
}
resetWorkingCopy() {
this._innerResetWorkingCopy();
this._workingCopyChanged();
}
_innerResetWorkingCopy() {
this._workingCopy = null;
this._workingCopyGetter = null;
}
/**
* @param {string} newWorkingCopy
*/
setWorkingCopy(newWorkingCopy) {
this._workingCopy = newWorkingCopy;
this._workingCopyGetter = null;
this._workingCopyChanged();
}
/**
* @param {string} content
* @param {boolean} isBase64
*/
setContent(content, isBase64) {
this._contentEncoded = isBase64;
if (this._project.canSetFileContent()) {
this._project.setFileContent(this, content, isBase64);
}
this._contentCommitted(content, true);
}
/**
* @param {function(): string } workingCopyGetter
*/
setWorkingCopyGetter(workingCopyGetter) {
this._workingCopyGetter = workingCopyGetter;
this._workingCopyChanged();
}
_workingCopyChanged() {
this._removeAllMessages();
this.dispatchEventToListeners(Events.WorkingCopyChanged, this);
this._project.workspace().dispatchEventToListeners(
Workspace.Workspace.Events.WorkingCopyChanged, {uiSourceCode: this});
}
removeWorkingCopyGetter() {
if (!this._workingCopyGetter) {
return;
}
this._workingCopy = this._workingCopyGetter();
this._workingCopyGetter = null;
}
commitWorkingCopy() {
if (this.isDirty()) {
this._commitContent(this.workingCopy());
}
}
/**
* @return {boolean}
*/
isDirty() {
return this._workingCopy !== null || this._workingCopyGetter !== null;
}
/**
* @return {string}
*/
extension() {
return Common.ParsedURL.extractExtension(this._name);
}
/**
* @return {string}
*/
content() {
return (this._content && this._content.content) || '';
}
/**
* @return {?string}
*/
loadError() {
return (this._content && this._content.error);
}
/**
* @override
* @param {string} query
* @param {boolean} caseSensitive
* @param {boolean} isRegex
* @return {!Promise<!Array<!Common.ContentProvider.SearchMatch>>}
*/
searchInContent(query, caseSensitive, isRegex) {
const content = this.content();
if (!content) {
return this._project.searchInFileContent(this, query, caseSensitive, isRegex);
}
return Promise.resolve(Common.ContentProvider.performSearchInContent(content, query, caseSensitive, isRegex));
}
/**
* @return {boolean}
*/
contentLoaded() {
return this._contentLoaded;
}
/**
* @param {number} lineNumber
* @param {number=} columnNumber
* @return {!UILocation}
*/
uiLocation(lineNumber, columnNumber) {
if (typeof columnNumber === 'undefined') {
columnNumber = 0;
}
return new UILocation(this, lineNumber, columnNumber);
}
/**
* @return {!Set<!Message>}
*/
messages() {
return this._messages ? new Set(this._messages) : new Set();
}
/**
* @param {!Message.Level} level
* @param {string} text
* @param {number} lineNumber
* @param {number=} columnNumber
* @return {!Message} message
*/
addLineMessage(level, text, lineNumber, columnNumber) {
return this.addMessage(
level, text, new TextUtils.TextRange(lineNumber, columnNumber || 0, lineNumber, columnNumber || 0));
}
/**
* @param {!Message.Level} level
* @param {string} text
* @param {!TextUtils.TextRange} range
* @return {!Message} message
*/
addMessage(level, text, range) {
const message = new Message(this, level, text, range);
if (!this._messages) {
this._messages = new Set();
}
this._messages.add(message);
this.dispatchEventToListeners(Events.MessageAdded, message);
return message;
}
/**
* @param {!Message} message
*/
removeMessage(message) {
if (this._messages && this._messages.delete(message)) {
this.dispatchEventToListeners(Events.MessageRemoved, message);
}
}
_removeAllMessages() {
if (!this._messages) {
return;
}
for (const message of this._messages) {
this.dispatchEventToListeners(Events.MessageRemoved, message);
}
this._messages = null;
}
/**
* @param {number} lineNumber
* @param {string} type
* @param {?} data
*/
addLineDecoration(lineNumber, type, data) {
this.addDecoration(TextUtils.TextRange.createFromLocation(lineNumber, 0), type, data);
}
/**
* @param {!TextUtils.TextRange} range
* @param {string} type
* @param {?} data
*/
addDecoration(range, type, data) {
const marker = new LineMarker(range, type, data);
if (!this._decorations) {
this._decorations = new Platform.Multimap();
}
this._decorations.set(type, marker);
this.dispatchEventToListeners(Events.LineDecorationAdded, marker);
}
/**
* @param {string} type
*/
removeDecorationsForType(type) {
if (!this._decorations) {
return;
}
const markers = this._decorations.get(type);
this._decorations.deleteAll(type);
markers.forEach(marker => {
this.dispatchEventToListeners(Events.LineDecorationRemoved, marker);
});
}
/**
* @return {!Array<!LineMarker>}
*/
allDecorations() {
return this._decorations ? this._decorations.valuesArray() : [];
}
removeAllDecorations() {
if (!this._decorations) {
return;
}
const decorationList = this._decorations.valuesArray();
this._decorations.clear();
decorationList.forEach(marker => this.dispatchEventToListeners(Events.LineDecorationRemoved, marker));
}
/**
* @param {string} type
* @return {?Set<!LineMarker>}
*/
decorationsForType(type) {
return this._decorations ? this._decorations.get(type) : null;
}
}
/** @enum {symbol} */
export const Events = {
WorkingCopyChanged: Symbol('WorkingCopyChanged'),
WorkingCopyCommitted: Symbol('WorkingCopyCommitted'),
TitleChanged: Symbol('TitleChanged'),
MessageAdded: Symbol('MessageAdded'),
MessageRemoved: Symbol('MessageRemoved'),
LineDecorationAdded: Symbol('LineDecorationAdded'),
LineDecorationRemoved: Symbol('LineDecorationRemoved')
};
/**
* @unrestricted
*/
export class UILocation {
/**
* @param {!UISourceCode} uiSourceCode
* @param {number} lineNumber
* @param {number} columnNumber
*/
constructor(uiSourceCode, lineNumber, columnNumber) {
this.uiSourceCode = uiSourceCode;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
}
/**
* @param {boolean=} skipTrim
* @return {string}
*/
linkText(skipTrim) {
let linkText = this.uiSourceCode.displayName(skipTrim);
if (typeof this.lineNumber === 'number') {
linkText += ':' + (this.lineNumber + 1);
}
return linkText;
}
/**
* @return {string}
*/
id() {
return this.uiSourceCode.project().id() + ':' + this.uiSourceCode.url() + ':' + this.lineNumber + ':' +
this.columnNumber;
}
/**
* @return {string}
*/
toUIString() {
return this.uiSourceCode.url() + ':' + (this.lineNumber + 1);
}
/**
* @param {!UILocation} location1
* @param {!UILocation} location2
* @return {number}
*/
static comparator(location1, location2) {
return location1.compareTo(location2);
}
/**
* @param {!UILocation} other
* @return {number}
*/
compareTo(other) {
if (this.uiSourceCode.url() !== other.uiSourceCode.url()) {
return this.uiSourceCode.url() > other.uiSourceCode.url() ? 1 : -1;
}
if (this.lineNumber !== other.lineNumber) {
return this.lineNumber - other.lineNumber;
}
return this.columnNumber - other.columnNumber;
}
}
/**
* @unrestricted
*/
export class Message {
/**
* @param {!UISourceCode} uiSourceCode
* @param {!Message.Level} level
* @param {string} text
* @param {!TextUtils.TextRange} range
*/
constructor(uiSourceCode, level, text, range) {
this._uiSourceCode = uiSourceCode;
this._level = level;
this._text = text;
this._range = range;
}
/**
* @return {!UISourceCode}
*/
uiSourceCode() {
return this._uiSourceCode;
}
/**
* @return {!Message.Level}
*/
level() {
return this._level;
}
/**
* @return {string}
*/
text() {
return this._text;
}
/**
* @return {!TextUtils.TextRange}
*/
range() {
return this._range;
}
/**
* @return {number}
*/
lineNumber() {
return this._range.startLine;
}
/**
* @return {(number|undefined)}
*/
columnNumber() {
return this._range.startColumn;
}
/**
* @param {!Message} another
* @return {boolean}
*/
isEqual(another) {
return this._uiSourceCode === another._uiSourceCode && this.text() === another.text() &&
this.level() === another.level() && this.range().equal(another.range());
}
remove() {
this._uiSourceCode.removeMessage(this);
}
}
/**
* @enum {string}
*/
Message.Level = {
Error: 'Error',
Warning: 'Warning'
};
/**
* @unrestricted
*/
export class LineMarker {
/**
* @param {!TextUtils.TextRange} range
* @param {string} type
* @param {?} data
*/
constructor(range, type, data) {
this._range = range;
this._type = type;
this._data = data;
}
/**
* @return {!TextUtils.TextRange}
*/
range() {
return this._range;
}
/**
* @return {string}
*/
type() {
return this._type;
}
/**
* @return {*}
*/
data() {
return this._data;
}
}
/**
* @unrestricted
*/
export class UISourceCodeMetadata {
/**
* @param {?Date} modificationTime
* @param {?number} contentSize
*/
constructor(modificationTime, contentSize) {
this.modificationTime = modificationTime;
this.contentSize = contentSize;
}
}
/* Legacy exported object */
self.Workspace = self.Workspace || {};
/* Legacy exported object */
Workspace = Workspace || {};
/** @constructor */
Workspace.UISourceCode = UISourceCode;
/** @enum {symbol} */
Workspace.UISourceCode.Events = Events;
/** @constructor */
Workspace.UISourceCode.Message = Message;
/** @constructor */
Workspace.UISourceCode.LineMarker = LineMarker;
/** @constructor */
Workspace.UILocation = UILocation;
/** @constructor */
Workspace.UISourceCodeMetadata = UISourceCodeMetadata;