1 /* 2 * Copyright (C) 2012 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 * @constructor 33 * @param {string} parentPath 34 * @param {string} name 35 * @param {string} originURL 36 * @param {string} url 37 * @param {!WebInspector.ResourceType} contentType 38 * @param {boolean} isEditable 39 * @param {boolean=} isContentScript 40 */ 41 WebInspector.FileDescriptor = function(parentPath, name, originURL, url, contentType, isEditable, isContentScript) 42 { 43 this.parentPath = parentPath; 44 this.name = name; 45 this.originURL = originURL; 46 this.url = url; 47 this.contentType = contentType; 48 this.isEditable = isEditable; 49 this.isContentScript = isContentScript || false; 50 } 51 52 /** 53 * @interface 54 * @extends {WebInspector.EventTarget} 55 */ 56 WebInspector.ProjectDelegate = function() { } 57 58 WebInspector.ProjectDelegate.Events = { 59 FileAdded: "FileAdded", 60 FileRemoved: "FileRemoved", 61 Reset: "Reset", 62 } 63 64 WebInspector.ProjectDelegate.prototype = { 65 /** 66 * @return {string} 67 */ 68 id: function() { }, 69 70 /** 71 * @return {string} 72 */ 73 type: function() { }, 74 75 /** 76 * @return {string} 77 */ 78 displayName: function() { }, 79 80 /** 81 * @param {string} path 82 * @param {function(?Date, ?number)} callback 83 */ 84 requestMetadata: function(path, callback) { }, 85 86 /** 87 * @param {string} path 88 * @param {function(?string)} callback 89 */ 90 requestFileContent: function(path, callback) { }, 91 92 /** 93 * @return {boolean} 94 */ 95 canSetFileContent: function() { }, 96 97 /** 98 * @param {string} path 99 * @param {string} newContent 100 * @param {function(?string)} callback 101 */ 102 setFileContent: function(path, newContent, callback) { }, 103 104 /** 105 * @return {boolean} 106 */ 107 canRename: function() { }, 108 109 /** 110 * @param {string} path 111 * @param {string} newName 112 * @param {function(boolean, string=, string=, string=, !WebInspector.ResourceType=)} callback 113 */ 114 rename: function(path, newName, callback) { }, 115 116 /** 117 * @param {string} path 118 */ 119 refresh: function(path) { }, 120 121 /** 122 * @param {string} path 123 */ 124 excludeFolder: function(path) { }, 125 126 /** 127 * @param {string} path 128 * @param {?string} name 129 * @param {string} content 130 * @param {function(?string)} callback 131 */ 132 createFile: function(path, name, content, callback) { }, 133 134 /** 135 * @param {string} path 136 */ 137 deleteFile: function(path) { }, 138 139 remove: function() { }, 140 141 /** 142 * @param {string} path 143 * @param {string} query 144 * @param {boolean} caseSensitive 145 * @param {boolean} isRegex 146 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback 147 */ 148 searchInFileContent: function(path, query, caseSensitive, isRegex, callback) { }, 149 150 /** 151 * @param {!Array.<string>} queries 152 * @param {!Array.<string>} fileQueries 153 * @param {boolean} caseSensitive 154 * @param {boolean} isRegex 155 * @param {!WebInspector.Progress} progress 156 * @param {function(!Array.<string>)} callback 157 */ 158 findFilesMatchingSearchRequest: function(queries, fileQueries, caseSensitive, isRegex, progress, callback) { }, 159 160 /** 161 * @param {!WebInspector.Progress} progress 162 * @param {function()} callback 163 */ 164 indexContent: function(progress, callback) { } 165 } 166 167 /** 168 * @param {!WebInspector.Workspace} workspace 169 * @param {!WebInspector.ProjectDelegate} projectDelegate 170 * @constructor 171 */ 172 WebInspector.Project = function(workspace, projectDelegate) 173 { 174 /** @type {!Object.<string, {uiSourceCode: !WebInspector.UISourceCode, index: number}>} */ 175 this._uiSourceCodesMap = {}; 176 /** @type {!Array.<!WebInspector.UISourceCode>} */ 177 this._uiSourceCodesList = []; 178 this._workspace = workspace; 179 this._projectDelegate = projectDelegate; 180 this._displayName = this._projectDelegate.displayName(); 181 this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.FileAdded, this._fileAdded, this); 182 this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.FileRemoved, this._fileRemoved, this); 183 this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.Reset, this._reset, this); 184 } 185 186 WebInspector.Project.prototype = { 187 /** 188 * @return {string} 189 */ 190 id: function() 191 { 192 return this._projectDelegate.id(); 193 }, 194 195 /** 196 * @return {string} 197 */ 198 type: function() 199 { 200 return this._projectDelegate.type(); 201 }, 202 203 /** 204 * @return {string} 205 */ 206 displayName: function() 207 { 208 return this._displayName; 209 }, 210 211 /** 212 * @return {boolean} 213 */ 214 isServiceProject: function() 215 { 216 return this._projectDelegate.type() === WebInspector.projectTypes.Debugger || this._projectDelegate.type() === WebInspector.projectTypes.LiveEdit; 217 }, 218 219 _fileAdded: function(event) 220 { 221 var fileDescriptor = /** @type {!WebInspector.FileDescriptor} */ (event.data); 222 var path = fileDescriptor.parentPath ? fileDescriptor.parentPath + "/" + fileDescriptor.name : fileDescriptor.name; 223 var uiSourceCode = this.uiSourceCode(path); 224 if (uiSourceCode) 225 return; 226 227 uiSourceCode = new WebInspector.UISourceCode(this, fileDescriptor.parentPath, fileDescriptor.name, fileDescriptor.originURL, fileDescriptor.url, fileDescriptor.contentType, fileDescriptor.isEditable); 228 uiSourceCode.isContentScript = fileDescriptor.isContentScript; 229 230 this._uiSourceCodesMap[path] = {uiSourceCode: uiSourceCode, index: this._uiSourceCodesList.length}; 231 this._uiSourceCodesList.push(uiSourceCode); 232 this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeAdded, uiSourceCode); 233 }, 234 235 _fileRemoved: function(event) 236 { 237 var path = /** @type {string} */ (event.data); 238 this._removeFile(path); 239 }, 240 241 /** 242 * @param {string} path 243 */ 244 _removeFile: function(path) 245 { 246 var uiSourceCode = this.uiSourceCode(path); 247 if (!uiSourceCode) 248 return; 249 250 var entry = this._uiSourceCodesMap[path]; 251 var movedUISourceCode = this._uiSourceCodesList[this._uiSourceCodesList.length - 1]; 252 this._uiSourceCodesList[entry.index] = movedUISourceCode; 253 var movedEntry = this._uiSourceCodesMap[movedUISourceCode.path()]; 254 movedEntry.index = entry.index; 255 this._uiSourceCodesList.splice(this._uiSourceCodesList.length - 1, 1); 256 delete this._uiSourceCodesMap[path]; 257 this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeRemoved, entry.uiSourceCode); 258 }, 259 260 _reset: function() 261 { 262 this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.ProjectWillReset, this); 263 this._uiSourceCodesMap = {}; 264 this._uiSourceCodesList = []; 265 }, 266 267 /** 268 * @param {string} path 269 * @return {?WebInspector.UISourceCode} 270 */ 271 uiSourceCode: function(path) 272 { 273 var entry = this._uiSourceCodesMap[path]; 274 return entry ? entry.uiSourceCode : null; 275 }, 276 277 /** 278 * @param {string} originURL 279 * @return {?WebInspector.UISourceCode} 280 */ 281 uiSourceCodeForOriginURL: function(originURL) 282 { 283 for (var i = 0; i < this._uiSourceCodesList.length; ++i) { 284 var uiSourceCode = this._uiSourceCodesList[i]; 285 if (uiSourceCode.originURL() === originURL) 286 return uiSourceCode; 287 } 288 return null; 289 }, 290 291 /** 292 * @return {!Array.<!WebInspector.UISourceCode>} 293 */ 294 uiSourceCodes: function() 295 { 296 return this._uiSourceCodesList; 297 }, 298 299 /** 300 * @param {!WebInspector.UISourceCode} uiSourceCode 301 * @param {function(?Date, ?number)} callback 302 */ 303 requestMetadata: function(uiSourceCode, callback) 304 { 305 this._projectDelegate.requestMetadata(uiSourceCode.path(), callback); 306 }, 307 308 /** 309 * @param {!WebInspector.UISourceCode} uiSourceCode 310 * @param {function(?string)} callback 311 */ 312 requestFileContent: function(uiSourceCode, callback) 313 { 314 this._projectDelegate.requestFileContent(uiSourceCode.path(), callback); 315 }, 316 317 /** 318 * @return {boolean} 319 */ 320 canSetFileContent: function() 321 { 322 return this._projectDelegate.canSetFileContent(); 323 }, 324 325 /** 326 * @param {!WebInspector.UISourceCode} uiSourceCode 327 * @param {string} newContent 328 * @param {function(?string)} callback 329 */ 330 setFileContent: function(uiSourceCode, newContent, callback) 331 { 332 this._projectDelegate.setFileContent(uiSourceCode.path(), newContent, onSetContent.bind(this)); 333 334 /** 335 * @param {?string} content 336 * @this {WebInspector.Project} 337 */ 338 function onSetContent(content) 339 { 340 this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeContentCommitted, { uiSourceCode: uiSourceCode, content: newContent }); 341 callback(content); 342 } 343 }, 344 345 /** 346 * @return {boolean} 347 */ 348 canRename: function() 349 { 350 return this._projectDelegate.canRename(); 351 }, 352 353 /** 354 * @param {!WebInspector.UISourceCode} uiSourceCode 355 * @param {string} newName 356 * @param {function(boolean, string=, string=, string=, !WebInspector.ResourceType=)} callback 357 */ 358 rename: function(uiSourceCode, newName, callback) 359 { 360 if (newName === uiSourceCode.name()) { 361 callback(true, uiSourceCode.name(), uiSourceCode.url, uiSourceCode.originURL(), uiSourceCode.contentType()); 362 return; 363 } 364 365 this._projectDelegate.rename(uiSourceCode.path(), newName, innerCallback.bind(this)); 366 367 /** 368 * @param {boolean} success 369 * @param {string=} newName 370 * @param {string=} newURL 371 * @param {string=} newOriginURL 372 * @param {!WebInspector.ResourceType=} newContentType 373 * @this {WebInspector.Project} 374 */ 375 function innerCallback(success, newName, newURL, newOriginURL, newContentType) 376 { 377 if (!success || !newName) { 378 callback(false); 379 return; 380 } 381 var oldPath = uiSourceCode.path(); 382 var newPath = uiSourceCode.parentPath() ? uiSourceCode.parentPath() + "/" + newName : newName; 383 this._uiSourceCodesMap[newPath] = this._uiSourceCodesMap[oldPath]; 384 delete this._uiSourceCodesMap[oldPath]; 385 callback(true, newName, newURL, newOriginURL, newContentType); 386 } 387 }, 388 389 /** 390 * @param {string} path 391 */ 392 refresh: function(path) 393 { 394 this._projectDelegate.refresh(path); 395 }, 396 397 /** 398 * @param {string} path 399 */ 400 excludeFolder: function(path) 401 { 402 this._projectDelegate.excludeFolder(path); 403 var uiSourceCodes = this._uiSourceCodesList.slice(); 404 for (var i = 0; i < uiSourceCodes.length; ++i) { 405 var uiSourceCode = uiSourceCodes[i]; 406 if (uiSourceCode.path().startsWith(path.substr(1))) 407 this._removeFile(uiSourceCode.path()); 408 } 409 }, 410 411 /** 412 * @param {string} path 413 * @param {?string} name 414 * @param {string} content 415 * @param {function(?string)} callback 416 */ 417 createFile: function(path, name, content, callback) 418 { 419 this._projectDelegate.createFile(path, name, content, innerCallback); 420 421 function innerCallback(filePath) 422 { 423 callback(filePath); 424 } 425 }, 426 427 /** 428 * @param {string} path 429 */ 430 deleteFile: function(path) 431 { 432 this._projectDelegate.deleteFile(path); 433 }, 434 435 remove: function() 436 { 437 this._projectDelegate.remove(); 438 }, 439 440 /** 441 * @param {!WebInspector.UISourceCode} uiSourceCode 442 * @param {string} query 443 * @param {boolean} caseSensitive 444 * @param {boolean} isRegex 445 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback 446 */ 447 searchInFileContent: function(uiSourceCode, query, caseSensitive, isRegex, callback) 448 { 449 this._projectDelegate.searchInFileContent(uiSourceCode.path(), query, caseSensitive, isRegex, callback); 450 }, 451 452 /** 453 * @param {!Array.<string>} queries 454 * @param {!Array.<string>} fileQueries 455 * @param {boolean} caseSensitive 456 * @param {boolean} isRegex 457 * @param {!WebInspector.Progress} progress 458 * @param {function(!Array.<string>)} callback 459 */ 460 findFilesMatchingSearchRequest: function(queries, fileQueries, caseSensitive, isRegex, progress, callback) 461 { 462 this._projectDelegate.findFilesMatchingSearchRequest(queries, fileQueries, caseSensitive, isRegex, progress, callback); 463 }, 464 465 /** 466 * @param {!WebInspector.Progress} progress 467 * @param {function()} callback 468 */ 469 indexContent: function(progress, callback) 470 { 471 this._projectDelegate.indexContent(progress, callback); 472 }, 473 474 dispose: function() 475 { 476 this._projectDelegate.reset(); 477 } 478 } 479 480 WebInspector.projectTypes = { 481 Debugger: "debugger", 482 LiveEdit: "liveedit", 483 Network: "network", 484 Snippets: "snippets", 485 FileSystem: "filesystem" 486 } 487 488 /** 489 * @constructor 490 * @extends {WebInspector.Object} 491 * @param {!WebInspector.FileSystemMapping} fileSystemMapping 492 */ 493 WebInspector.Workspace = function(fileSystemMapping) 494 { 495 this._fileSystemMapping = fileSystemMapping; 496 /** @type {!Object.<string, !WebInspector.Project>} */ 497 this._projects = {}; 498 } 499 500 WebInspector.Workspace.Events = { 501 UISourceCodeAdded: "UISourceCodeAdded", 502 UISourceCodeRemoved: "UISourceCodeRemoved", 503 UISourceCodeContentCommitted: "UISourceCodeContentCommitted", 504 ProjectWillReset: "ProjectWillReset" 505 } 506 507 WebInspector.Workspace.prototype = { 508 /** 509 * @return {!Array.<!WebInspector.UISourceCode>} 510 */ 511 unsavedSourceCodes: function() 512 { 513 function filterUnsaved(sourceCode) 514 { 515 return sourceCode.isDirty(); 516 } 517 return this.uiSourceCodes().filter(filterUnsaved); 518 }, 519 520 /** 521 * @param {string} projectId 522 * @param {string} path 523 * @return {?WebInspector.UISourceCode} 524 */ 525 uiSourceCode: function(projectId, path) 526 { 527 var project = this._projects[projectId]; 528 return project ? project.uiSourceCode(path) : null; 529 }, 530 531 /** 532 * @param {string} originURL 533 * @return {?WebInspector.UISourceCode} 534 */ 535 uiSourceCodeForOriginURL: function(originURL) 536 { 537 var networkProjects = this.projectsForType(WebInspector.projectTypes.Network) 538 for (var i = 0; i < networkProjects.length; ++i) { 539 var project = networkProjects[i]; 540 var uiSourceCode = project.uiSourceCodeForOriginURL(originURL); 541 if (uiSourceCode) 542 return uiSourceCode; 543 } 544 return null; 545 }, 546 547 /** 548 * @param {string} type 549 * @return {!Array.<!WebInspector.UISourceCode>} 550 */ 551 uiSourceCodesForProjectType: function(type) 552 { 553 var result = []; 554 for (var projectName in this._projects) { 555 var project = this._projects[projectName]; 556 if (project.type() === type) 557 result = result.concat(project.uiSourceCodes()); 558 } 559 return result; 560 }, 561 562 /** 563 * @param {!WebInspector.ProjectDelegate} projectDelegate 564 * @return {!WebInspector.Project} 565 */ 566 addProject: function(projectDelegate) 567 { 568 var projectId = projectDelegate.id(); 569 this._projects[projectId] = new WebInspector.Project(this, projectDelegate); 570 return this._projects[projectId]; 571 }, 572 573 /** 574 * @param {string} projectId 575 */ 576 removeProject: function(projectId) 577 { 578 var project = this._projects[projectId]; 579 if (!project) 580 return; 581 project.dispose(); 582 delete this._projects[projectId]; 583 }, 584 585 /** 586 * @param {string} projectId 587 * @return {!WebInspector.Project} 588 */ 589 project: function(projectId) 590 { 591 return this._projects[projectId]; 592 }, 593 594 /** 595 * @return {!Array.<!WebInspector.Project>} 596 */ 597 projects: function() 598 { 599 return Object.values(this._projects); 600 }, 601 602 /** 603 * @param {string} type 604 * @return {!Array.<!WebInspector.Project>} 605 */ 606 projectsForType: function(type) 607 { 608 function filterByType(project) 609 { 610 return project.type() === type; 611 } 612 return this.projects().filter(filterByType); 613 }, 614 615 /** 616 * @return {!Array.<!WebInspector.UISourceCode>} 617 */ 618 uiSourceCodes: function() 619 { 620 var result = []; 621 for (var projectId in this._projects) { 622 var project = this._projects[projectId]; 623 result = result.concat(project.uiSourceCodes()); 624 } 625 return result; 626 }, 627 628 /** 629 * @param {string} url 630 * @return {boolean} 631 */ 632 hasMappingForURL: function(url) 633 { 634 if (!InspectorFrontendHost.supportsFileSystems()) 635 return false; 636 return this._fileSystemMapping.hasMappingForURL(url); 637 }, 638 639 /** 640 * @param {string} url 641 * @return {?WebInspector.UISourceCode} 642 */ 643 _networkUISourceCodeForURL: function(url) 644 { 645 var splitURL = WebInspector.ParsedURL.splitURL(url); 646 var projectId = WebInspector.SimpleProjectDelegate.projectId(splitURL[0], WebInspector.projectTypes.Network); 647 var project = this.project(projectId); 648 return project ? project.uiSourceCode(splitURL.slice(1).join("/")) : null; 649 }, 650 651 /** 652 * @param {string} url 653 * @return {?WebInspector.UISourceCode} 654 */ 655 uiSourceCodeForURL: function(url) 656 { 657 if (!InspectorFrontendHost.supportsFileSystems()) 658 return this._networkUISourceCodeForURL(url); 659 var file = this._fileSystemMapping.fileForURL(url); 660 if (!file) 661 return this._networkUISourceCodeForURL(url); 662 663 var projectId = WebInspector.FileSystemProjectDelegate.projectId(file.fileSystemPath); 664 var project = this.project(projectId); 665 return project ? project.uiSourceCode(file.filePath) : null; 666 }, 667 668 /** 669 * @param {string} fileSystemPath 670 * @param {string} filePath 671 * @return {string} 672 */ 673 urlForPath: function(fileSystemPath, filePath) 674 { 675 return this._fileSystemMapping.urlForPath(fileSystemPath, filePath); 676 }, 677 678 /** 679 * @param {!WebInspector.UISourceCode} networkUISourceCode 680 * @param {!WebInspector.UISourceCode} uiSourceCode 681 * @param {!WebInspector.FileSystemWorkspaceProvider} fileSystemWorkspaceProvider 682 */ 683 addMapping: function(networkUISourceCode, uiSourceCode, fileSystemWorkspaceProvider) 684 { 685 var url = networkUISourceCode.url; 686 var path = uiSourceCode.path(); 687 var fileSystemPath = fileSystemWorkspaceProvider.fileSystemPath(uiSourceCode); 688 this._fileSystemMapping.addMappingForResource(url, fileSystemPath, path); 689 WebInspector.suggestReload(); 690 }, 691 692 /** 693 * @param {!WebInspector.UISourceCode} uiSourceCode 694 */ 695 removeMapping: function(uiSourceCode) 696 { 697 this._fileSystemMapping.removeMappingForURL(uiSourceCode.url); 698 WebInspector.suggestReload(); 699 }, 700 701 __proto__: WebInspector.Object.prototype 702 } 703 704 /** 705 * @type {!WebInspector.Workspace} 706 */ 707 WebInspector.workspace; 708