1 /* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 32 /** 33 * @constructor 34 * @extends {WebInspector.Object} 35 * @implements {WebInspector.ContentProvider} 36 * @param {!WebInspector.Project} project 37 * @param {string} parentPath 38 * @param {string} name 39 * @param {string} originURL 40 * @param {string} url 41 * @param {!WebInspector.ResourceType} contentType 42 */ 43 WebInspector.UISourceCode = function(project, parentPath, name, originURL, url, contentType) 44 { 45 this._project = project; 46 this._parentPath = parentPath; 47 this._name = name; 48 this._originURL = originURL; 49 this._url = url; 50 this._contentType = contentType; 51 /** @type {!Array.<function(?string)>} */ 52 this._requestContentCallbacks = []; 53 54 /** @type {!Array.<!WebInspector.Revision>} */ 55 this.history = []; 56 } 57 58 /** 59 * @enum {string} 60 */ 61 WebInspector.UISourceCode.Events = { 62 WorkingCopyChanged: "WorkingCopyChanged", 63 WorkingCopyCommitted: "WorkingCopyCommitted", 64 TitleChanged: "TitleChanged", 65 SavedStateUpdated: "SavedStateUpdated", 66 SourceMappingChanged: "SourceMappingChanged", 67 } 68 69 WebInspector.UISourceCode.prototype = { 70 /** 71 * @return {string} 72 */ 73 get url() 74 { 75 return this._url; 76 }, 77 78 /** 79 * @return {string} 80 */ 81 name: function() 82 { 83 return this._name; 84 }, 85 86 /** 87 * @return {string} 88 */ 89 parentPath: function() 90 { 91 return this._parentPath; 92 }, 93 94 /** 95 * @return {string} 96 */ 97 path: function() 98 { 99 return this._parentPath ? this._parentPath + "/" + this._name : this._name; 100 }, 101 102 /** 103 * @return {string} 104 */ 105 fullDisplayName: function() 106 { 107 return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true); 108 }, 109 110 /** 111 * @param {boolean=} skipTrim 112 * @return {string} 113 */ 114 displayName: function(skipTrim) 115 { 116 var displayName = this.name() || WebInspector.UIString("(index)"); 117 return skipTrim ? displayName : displayName.trimEnd(100); 118 }, 119 120 /** 121 * @return {string} 122 */ 123 uri: function() 124 { 125 var path = this.path(); 126 if (!this._project.id()) 127 return path; 128 if (!path) 129 return this._project.id(); 130 return this._project.id() + "/" + path; 131 }, 132 133 /** 134 * @return {string} 135 */ 136 originURL: function() 137 { 138 return this._originURL; 139 }, 140 141 /** 142 * @return {boolean} 143 */ 144 canRename: function() 145 { 146 return this._project.canRename(); 147 }, 148 149 /** 150 * @param {string} newName 151 * @param {function(boolean)} callback 152 */ 153 rename: function(newName, callback) 154 { 155 this._project.rename(this, newName, innerCallback.bind(this)); 156 157 /** 158 * @param {boolean} success 159 * @param {string=} newName 160 * @param {string=} newURL 161 * @param {string=} newOriginURL 162 * @param {!WebInspector.ResourceType=} newContentType 163 * @this {WebInspector.UISourceCode} 164 */ 165 function innerCallback(success, newName, newURL, newOriginURL, newContentType) 166 { 167 if (success) 168 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType)); 169 callback(success); 170 } 171 }, 172 173 remove: function() 174 { 175 this._project.deleteFile(this.path()); 176 }, 177 178 /** 179 * @param {string} name 180 * @param {string} url 181 * @param {string} originURL 182 * @param {!WebInspector.ResourceType=} contentType 183 */ 184 _updateName: function(name, url, originURL, contentType) 185 { 186 var oldURI = this.uri(); 187 this._name = name; 188 if (url) 189 this._url = url; 190 if (originURL) 191 this._originURL = originURL; 192 if (contentType) 193 this._contentType = contentType; 194 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI); 195 }, 196 197 /** 198 * @return {string} 199 */ 200 contentURL: function() 201 { 202 return this.originURL(); 203 }, 204 205 /** 206 * @return {!WebInspector.ResourceType} 207 */ 208 contentType: function() 209 { 210 return this._contentType; 211 }, 212 213 /** 214 * @return {!WebInspector.Project} 215 */ 216 project: function() 217 { 218 return this._project; 219 }, 220 221 /** 222 * @param {function(?Date, ?number)} callback 223 */ 224 requestMetadata: function(callback) 225 { 226 this._project.requestMetadata(this, callback); 227 }, 228 229 /** 230 * @param {function(?string)} callback 231 */ 232 requestContent: function(callback) 233 { 234 if (this._content || this._contentLoaded) { 235 callback(this._content); 236 return; 237 } 238 this._requestContentCallbacks.push(callback); 239 if (this._requestContentCallbacks.length === 1) 240 this._project.requestFileContent(this, this._fireContentAvailable.bind(this)); 241 }, 242 243 /** 244 * @param {function()} callback 245 */ 246 _pushCheckContentUpdatedCallback: function(callback) 247 { 248 if (!this._checkContentUpdatedCallbacks) 249 this._checkContentUpdatedCallbacks = []; 250 this._checkContentUpdatedCallbacks.push(callback); 251 }, 252 253 _terminateContentCheck: function() 254 { 255 delete this._checkingContent; 256 if (this._checkContentUpdatedCallbacks) { 257 this._checkContentUpdatedCallbacks.forEach(function(callback) { callback(); }); 258 delete this._checkContentUpdatedCallbacks; 259 } 260 }, 261 262 /** 263 * @param {function()=} callback 264 */ 265 checkContentUpdated: function(callback) 266 { 267 callback = callback || function() {}; 268 if (!this._project.canSetFileContent()) { 269 callback(); 270 return; 271 } 272 this._pushCheckContentUpdatedCallback(callback); 273 274 if (this._checkingContent) { 275 return; 276 } 277 this._checkingContent = true; 278 this._project.requestFileContent(this, contentLoaded.bind(this)); 279 280 /** 281 * @param {?string} updatedContent 282 * @this {WebInspector.UISourceCode} 283 */ 284 function contentLoaded(updatedContent) 285 { 286 if (updatedContent === null) { 287 var workingCopy = this.workingCopy(); 288 this._commitContent("", false); 289 this.setWorkingCopy(workingCopy); 290 this._terminateContentCheck(); 291 return; 292 } 293 if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) { 294 this._terminateContentCheck(); 295 return; 296 } 297 if (this._content === updatedContent) { 298 delete this._lastAcceptedContent; 299 this._terminateContentCheck(); 300 return; 301 } 302 303 if (!this.isDirty()) { 304 this._commitContent(updatedContent, false); 305 this._terminateContentCheck(); 306 return; 307 } 308 309 var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?")); 310 if (shouldUpdate) 311 this._commitContent(updatedContent, false); 312 else 313 this._lastAcceptedContent = updatedContent; 314 this._terminateContentCheck(); 315 } 316 }, 317 318 /** 319 * @param {function(?string)} callback 320 */ 321 requestOriginalContent: function(callback) 322 { 323 this._project.requestFileContent(this, callback); 324 }, 325 326 /** 327 * @param {string} content 328 * @param {boolean} shouldSetContentInProject 329 */ 330 _commitContent: function(content, shouldSetContentInProject) 331 { 332 delete this._lastAcceptedContent; 333 this._content = content; 334 this._contentLoaded = true; 335 336 var lastRevision = this.history.length ? this.history[this.history.length - 1] : null; 337 if (!lastRevision || lastRevision._content !== this._content) { 338 var revision = new WebInspector.Revision(this, this._content, new Date()); 339 this.history.push(revision); 340 } 341 342 this._innerResetWorkingCopy(); 343 this._hasCommittedChanges = true; 344 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted); 345 if (this._url && WebInspector.fileManager.isURLSaved(this._url)) 346 this._saveURLWithFileManager(false, this._content); 347 if (shouldSetContentInProject) 348 this._project.setFileContent(this, this._content, function() { }); 349 }, 350 351 /** 352 * @param {boolean} forceSaveAs 353 * @param {?string} content 354 */ 355 _saveURLWithFileManager: function(forceSaveAs, content) 356 { 357 WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this)); 358 WebInspector.fileManager.close(this._url); 359 360 /** 361 * @param {boolean} accepted 362 * @this {WebInspector.UISourceCode} 363 */ 364 function callback(accepted) 365 { 366 if (!accepted) 367 return; 368 this._savedWithFileManager = true; 369 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated); 370 } 371 }, 372 373 /** 374 * @param {boolean} forceSaveAs 375 */ 376 save: function(forceSaveAs) 377 { 378 if (this.project().type() === WebInspector.projectTypes.FileSystem || this.project().type() === WebInspector.projectTypes.Snippets) { 379 this.commitWorkingCopy(); 380 return; 381 } 382 if (this.isDirty()) { 383 this._saveURLWithFileManager(forceSaveAs, this.workingCopy()); 384 this.commitWorkingCopy(); 385 return; 386 } 387 this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs)); 388 }, 389 390 /** 391 * @return {boolean} 392 */ 393 hasUnsavedCommittedChanges: function() 394 { 395 if (this._savedWithFileManager || this.project().canSetFileContent() || this._project.isServiceProject()) 396 return false; 397 if (this._project.workspace().hasResourceContentTrackingExtensions()) 398 return false; 399 return !!this._hasCommittedChanges; 400 }, 401 402 /** 403 * @param {string} content 404 */ 405 addRevision: function(content) 406 { 407 this._commitContent(content, true); 408 }, 409 410 revertToOriginal: function() 411 { 412 /** 413 * @this {WebInspector.UISourceCode} 414 * @param {?string} content 415 */ 416 function callback(content) 417 { 418 if (typeof content !== "string") 419 return; 420 421 this.addRevision(content); 422 } 423 424 this.requestOriginalContent(callback.bind(this)); 425 }, 426 427 /** 428 * @param {function(!WebInspector.UISourceCode)} callback 429 */ 430 revertAndClearHistory: function(callback) 431 { 432 /** 433 * @this {WebInspector.UISourceCode} 434 * @param {?string} content 435 */ 436 function revert(content) 437 { 438 if (typeof content !== "string") 439 return; 440 441 this.addRevision(content); 442 this.history = []; 443 callback(this); 444 } 445 446 this.requestOriginalContent(revert.bind(this)); 447 }, 448 449 /** 450 * @return {string} 451 */ 452 workingCopy: function() 453 { 454 if (this._workingCopyGetter) { 455 this._workingCopy = this._workingCopyGetter(); 456 delete this._workingCopyGetter; 457 } 458 if (this.isDirty()) 459 return this._workingCopy; 460 return this._content; 461 }, 462 463 resetWorkingCopy: function() 464 { 465 this._innerResetWorkingCopy(); 466 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged); 467 }, 468 469 _innerResetWorkingCopy: function() 470 { 471 delete this._workingCopy; 472 delete this._workingCopyGetter; 473 }, 474 475 /** 476 * @param {string} newWorkingCopy 477 */ 478 setWorkingCopy: function(newWorkingCopy) 479 { 480 this._workingCopy = newWorkingCopy; 481 delete this._workingCopyGetter; 482 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged); 483 }, 484 485 setWorkingCopyGetter: function(workingCopyGetter) 486 { 487 this._workingCopyGetter = workingCopyGetter; 488 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged); 489 }, 490 491 removeWorkingCopyGetter: function() 492 { 493 if (!this._workingCopyGetter) 494 return; 495 this._workingCopy = this._workingCopyGetter(); 496 delete this._workingCopyGetter; 497 }, 498 499 commitWorkingCopy: function() 500 { 501 if (this.isDirty()) 502 this._commitContent(this.workingCopy(), true); 503 }, 504 505 /** 506 * @return {boolean} 507 */ 508 isDirty: function() 509 { 510 return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined"; 511 }, 512 513 /** 514 * @return {string} 515 */ 516 highlighterType: function() 517 { 518 if (this._project.type() === WebInspector.projectTypes.Network) 519 return this.contentType().canonicalMimeType(); 520 var lastIndexOfDot = this._name.lastIndexOf("."); 521 var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : ""; 522 var indexOfQuestionMark = extension.indexOf("?"); 523 if (indexOfQuestionMark !== -1) 524 extension = extension.substr(0, indexOfQuestionMark); 525 var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()]; 526 return mimeType || this.contentType().canonicalMimeType(); 527 }, 528 529 /** 530 * @return {?string} 531 */ 532 content: function() 533 { 534 return this._content; 535 }, 536 537 /** 538 * @param {string} query 539 * @param {boolean} caseSensitive 540 * @param {boolean} isRegex 541 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback 542 */ 543 searchInContent: function(query, caseSensitive, isRegex, callback) 544 { 545 var content = this.content(); 546 if (content) { 547 var provider = new WebInspector.StaticContentProvider(this.contentType(), content); 548 provider.searchInContent(query, caseSensitive, isRegex, callback); 549 return; 550 } 551 552 this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback); 553 }, 554 555 /** 556 * @param {?string} content 557 */ 558 _fireContentAvailable: function(content) 559 { 560 this._contentLoaded = true; 561 this._content = content; 562 563 var callbacks = this._requestContentCallbacks.slice(); 564 this._requestContentCallbacks = []; 565 for (var i = 0; i < callbacks.length; ++i) 566 callbacks[i](content); 567 }, 568 569 /** 570 * @return {boolean} 571 */ 572 contentLoaded: function() 573 { 574 return this._contentLoaded; 575 }, 576 577 /** 578 * @param {number} lineNumber 579 * @param {number=} columnNumber 580 * @return {!WebInspector.UILocation} 581 */ 582 uiLocation: function(lineNumber, columnNumber) 583 { 584 if (typeof columnNumber === "undefined") 585 columnNumber = 0; 586 return new WebInspector.UILocation(this, lineNumber, columnNumber); 587 }, 588 589 __proto__: WebInspector.Object.prototype 590 } 591 592 /** 593 * @constructor 594 * @param {!WebInspector.UISourceCode} uiSourceCode 595 * @param {number} lineNumber 596 * @param {number} columnNumber 597 */ 598 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber) 599 { 600 this.uiSourceCode = uiSourceCode; 601 this.lineNumber = lineNumber; 602 this.columnNumber = columnNumber; 603 } 604 605 WebInspector.UILocation.prototype = { 606 /** 607 * @return {string} 608 */ 609 linkText: function() 610 { 611 var linkText = this.uiSourceCode.displayName(); 612 if (typeof this.lineNumber === "number") 613 linkText += ":" + (this.lineNumber + 1); 614 return linkText; 615 }, 616 617 /** 618 * @return {string} 619 */ 620 id: function() 621 { 622 return this.uiSourceCode.uri() + ":" + this.lineNumber + ":" + this.columnNumber; 623 }, 624 } 625 626 /** 627 * @constructor 628 * @implements {WebInspector.ContentProvider} 629 * @param {!WebInspector.UISourceCode} uiSourceCode 630 * @param {?string|undefined} content 631 * @param {!Date} timestamp 632 */ 633 WebInspector.Revision = function(uiSourceCode, content, timestamp) 634 { 635 this._uiSourceCode = uiSourceCode; 636 this._content = content; 637 this._timestamp = timestamp; 638 } 639 640 WebInspector.Revision.prototype = { 641 /** 642 * @return {!WebInspector.UISourceCode} 643 */ 644 get uiSourceCode() 645 { 646 return this._uiSourceCode; 647 }, 648 649 /** 650 * @return {!Date} 651 */ 652 get timestamp() 653 { 654 return this._timestamp; 655 }, 656 657 /** 658 * @return {?string} 659 */ 660 get content() 661 { 662 return this._content || null; 663 }, 664 665 revertToThis: function() 666 { 667 /** 668 * @param {string} content 669 * @this {WebInspector.Revision} 670 */ 671 function revert(content) 672 { 673 if (this._uiSourceCode._content !== content) 674 this._uiSourceCode.addRevision(content); 675 } 676 this.requestContent(revert.bind(this)); 677 }, 678 679 /** 680 * @return {string} 681 */ 682 contentURL: function() 683 { 684 return this._uiSourceCode.originURL(); 685 }, 686 687 /** 688 * @return {!WebInspector.ResourceType} 689 */ 690 contentType: function() 691 { 692 return this._uiSourceCode.contentType(); 693 }, 694 695 /** 696 * @param {function(string)} callback 697 */ 698 requestContent: function(callback) 699 { 700 callback(this._content || ""); 701 }, 702 703 /** 704 * @param {string} query 705 * @param {boolean} caseSensitive 706 * @param {boolean} isRegex 707 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback 708 */ 709 searchInContent: function(query, caseSensitive, isRegex, callback) 710 { 711 callback([]); 712 } 713 } 714