workspace.js
1 const _ = require('underscore-plus'); 2 const url = require('url'); 3 const path = require('path'); 4 const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); 5 const fs = require('fs-plus'); 6 const { Directory } = require('pathwatcher'); 7 const Grim = require('grim'); 8 const DefaultDirectorySearcher = require('./default-directory-searcher'); 9 const RipgrepDirectorySearcher = require('./ripgrep-directory-searcher'); 10 const Dock = require('./dock'); 11 const Model = require('./model'); 12 const StateStore = require('./state-store'); 13 const TextEditor = require('./text-editor'); 14 const Panel = require('./panel'); 15 const PanelContainer = require('./panel-container'); 16 const Task = require('./task'); 17 const WorkspaceCenter = require('./workspace-center'); 18 const WorkspaceElement = require('./workspace-element'); 19 20 const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100; 21 const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom']; 22 23 // Essential: Represents the state of the user interface for the entire window. 24 // An instance of this class is available via the `atom.workspace` global. 25 // 26 // Interact with this object to open files, be notified of current and future 27 // editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} 28 // and friends. 29 // 30 // ## Workspace Items 31 // 32 // The term "item" refers to anything that can be displayed 33 // in a pane within the workspace, either in the {WorkspaceCenter} or in one 34 // of the three {Dock}s. The workspace expects items to conform to the 35 // following interface: 36 // 37 // ### Required Methods 38 // 39 // #### `getTitle()` 40 // 41 // Returns a {String} containing the title of the item to display on its 42 // associated tab. 43 // 44 // ### Optional Methods 45 // 46 // #### `getElement()` 47 // 48 // If your item already *is* a DOM element, you do not need to implement this 49 // method. Otherwise it should return the element you want to display to 50 // represent this item. 51 // 52 // #### `destroy()` 53 // 54 // Destroys the item. This will be called when the item is removed from its 55 // parent pane. 56 // 57 // #### `onDidDestroy(callback)` 58 // 59 // Called by the workspace so it can be notified when the item is destroyed. 60 // Must return a {Disposable}. 61 // 62 // #### `serialize()` 63 // 64 // Serialize the state of the item. Must return an object that can be passed to 65 // `JSON.stringify`. The state should include a field called `deserializer`, 66 // which names a deserializer declared in your `package.json`. This method is 67 // invoked on items when serializing the workspace so they can be restored to 68 // the same location later. 69 // 70 // #### `getURI()` 71 // 72 // Returns the URI associated with the item. 73 // 74 // #### `getLongTitle()` 75 // 76 // Returns a {String} containing a longer version of the title to display in 77 // places like the window title or on tabs their short titles are ambiguous. 78 // 79 // #### `onDidChangeTitle(callback)` 80 // 81 // Called by the workspace so it can be notified when the item's title changes. 82 // Must return a {Disposable}. 83 // 84 // #### `getIconName()` 85 // 86 // Return a {String} with the name of an icon. If this method is defined and 87 // returns a string, the item's tab element will be rendered with the `icon` and 88 // `icon-${iconName}` CSS classes. 89 // 90 // ### `onDidChangeIcon(callback)` 91 // 92 // Called by the workspace so it can be notified when the item's icon changes. 93 // Must return a {Disposable}. 94 // 95 // #### `getDefaultLocation()` 96 // 97 // Tells the workspace where your item should be opened in absence of a user 98 // override. Items can appear in the center or in a dock on the left, right, or 99 // bottom of the workspace. 100 // 101 // Returns a {String} with one of the following values: `'center'`, `'left'`, 102 // `'right'`, `'bottom'`. If this method is not defined, `'center'` is the 103 // default. 104 // 105 // #### `getAllowedLocations()` 106 // 107 // Tells the workspace where this item can be moved. Returns an {Array} of one 108 // or more of the following values: `'center'`, `'left'`, `'right'`, or 109 // `'bottom'`. 110 // 111 // #### `isPermanentDockItem()` 112 // 113 // Tells the workspace whether or not this item can be closed by the user by 114 // clicking an `x` on its tab. Use of this feature is discouraged unless there's 115 // a very good reason not to allow users to close your item. Items can be made 116 // permanent *only* when they are contained in docks. Center pane items can 117 // always be removed. Note that it is currently still possible to close dock 118 // items via the `Close Pane` option in the context menu and via Atom APIs, so 119 // you should still be prepared to handle your dock items being destroyed by the 120 // user even if you implement this method. 121 // 122 // #### `save()` 123 // 124 // Saves the item. 125 // 126 // #### `saveAs(path)` 127 // 128 // Saves the item to the specified path. 129 // 130 // #### `getPath()` 131 // 132 // Returns the local path associated with this item. This is only used to set 133 // the initial location of the "save as" dialog. 134 // 135 // #### `isModified()` 136 // 137 // Returns whether or not the item is modified to reflect modification in the 138 // UI. 139 // 140 // #### `onDidChangeModified()` 141 // 142 // Called by the workspace so it can be notified when item's modified status 143 // changes. Must return a {Disposable}. 144 // 145 // #### `copy()` 146 // 147 // Create a copy of the item. If defined, the workspace will call this method to 148 // duplicate the item when splitting panes via certain split commands. 149 // 150 // #### `getPreferredHeight()` 151 // 152 // If this item is displayed in the bottom {Dock}, called by the workspace when 153 // initially displaying the dock to set its height. Once the dock has been 154 // resized by the user, their height will override this value. 155 // 156 // Returns a {Number}. 157 // 158 // #### `getPreferredWidth()` 159 // 160 // If this item is displayed in the left or right {Dock}, called by the 161 // workspace when initially displaying the dock to set its width. Once the dock 162 // has been resized by the user, their width will override this value. 163 // 164 // Returns a {Number}. 165 // 166 // #### `onDidTerminatePendingState(callback)` 167 // 168 // If the workspace is configured to use *pending pane items*, the workspace 169 // will subscribe to this method to terminate the pending state of the item. 170 // Must return a {Disposable}. 171 // 172 // #### `shouldPromptToSave()` 173 // 174 // This method indicates whether Atom should prompt the user to save this item 175 // when the user closes or reloads the window. Returns a boolean. 176 module.exports = class Workspace extends Model { 177 constructor(params) { 178 super(...arguments); 179 180 this.updateWindowTitle = this.updateWindowTitle.bind(this); 181 this.updateDocumentEdited = this.updateDocumentEdited.bind(this); 182 this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this); 183 this.didChangeActivePaneOnPaneContainer = this.didChangeActivePaneOnPaneContainer.bind( 184 this 185 ); 186 this.didChangeActivePaneItemOnPaneContainer = this.didChangeActivePaneItemOnPaneContainer.bind( 187 this 188 ); 189 this.didActivatePaneContainer = this.didActivatePaneContainer.bind(this); 190 191 this.enablePersistence = params.enablePersistence; 192 this.packageManager = params.packageManager; 193 this.config = params.config; 194 this.project = params.project; 195 this.notificationManager = params.notificationManager; 196 this.viewRegistry = params.viewRegistry; 197 this.grammarRegistry = params.grammarRegistry; 198 this.applicationDelegate = params.applicationDelegate; 199 this.assert = params.assert; 200 this.deserializerManager = params.deserializerManager; 201 this.textEditorRegistry = params.textEditorRegistry; 202 this.styleManager = params.styleManager; 203 this.draggingItem = false; 204 this.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1); 205 206 this.emitter = new Emitter(); 207 this.openers = []; 208 this.destroyedItemURIs = []; 209 this.stoppedChangingActivePaneItemTimeout = null; 210 211 this.scandalDirectorySearcher = new DefaultDirectorySearcher(); 212 this.ripgrepDirectorySearcher = new RipgrepDirectorySearcher(); 213 this.consumeServices(this.packageManager); 214 215 this.paneContainers = { 216 center: this.createCenter(), 217 left: this.createDock('left'), 218 right: this.createDock('right'), 219 bottom: this.createDock('bottom') 220 }; 221 this.activePaneContainer = this.paneContainers.center; 222 this.hasActiveTextEditor = false; 223 224 this.panelContainers = { 225 top: new PanelContainer({ 226 viewRegistry: this.viewRegistry, 227 location: 'top' 228 }), 229 left: new PanelContainer({ 230 viewRegistry: this.viewRegistry, 231 location: 'left', 232 dock: this.paneContainers.left 233 }), 234 right: new PanelContainer({ 235 viewRegistry: this.viewRegistry, 236 location: 'right', 237 dock: this.paneContainers.right 238 }), 239 bottom: new PanelContainer({ 240 viewRegistry: this.viewRegistry, 241 location: 'bottom', 242 dock: this.paneContainers.bottom 243 }), 244 header: new PanelContainer({ 245 viewRegistry: this.viewRegistry, 246 location: 'header' 247 }), 248 footer: new PanelContainer({ 249 viewRegistry: this.viewRegistry, 250 location: 'footer' 251 }), 252 modal: new PanelContainer({ 253 viewRegistry: this.viewRegistry, 254 location: 'modal' 255 }) 256 }; 257 258 this.incoming = new Map(); 259 } 260 261 get paneContainer() { 262 Grim.deprecate( 263 '`atom.workspace.paneContainer` has always been private, but it is now gone. Please use `atom.workspace.getCenter()` instead and consult the workspace API docs for public methods.' 264 ); 265 return this.paneContainers.center.paneContainer; 266 } 267 268 getElement() { 269 if (!this.element) { 270 this.element = new WorkspaceElement().initialize(this, { 271 config: this.config, 272 project: this.project, 273 viewRegistry: this.viewRegistry, 274 styleManager: this.styleManager 275 }); 276 } 277 return this.element; 278 } 279 280 createCenter() { 281 return new WorkspaceCenter({ 282 config: this.config, 283 applicationDelegate: this.applicationDelegate, 284 notificationManager: this.notificationManager, 285 deserializerManager: this.deserializerManager, 286 viewRegistry: this.viewRegistry, 287 didActivate: this.didActivatePaneContainer, 288 didChangeActivePane: this.didChangeActivePaneOnPaneContainer, 289 didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer, 290 didDestroyPaneItem: this.didDestroyPaneItem 291 }); 292 } 293 294 createDock(location) { 295 return new Dock({ 296 location, 297 config: this.config, 298 applicationDelegate: this.applicationDelegate, 299 deserializerManager: this.deserializerManager, 300 notificationManager: this.notificationManager, 301 viewRegistry: this.viewRegistry, 302 didActivate: this.didActivatePaneContainer, 303 didChangeActivePane: this.didChangeActivePaneOnPaneContainer, 304 didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer, 305 didDestroyPaneItem: this.didDestroyPaneItem 306 }); 307 } 308 309 reset(packageManager) { 310 this.packageManager = packageManager; 311 this.emitter.dispose(); 312 this.emitter = new Emitter(); 313 314 this.paneContainers.center.destroy(); 315 this.paneContainers.left.destroy(); 316 this.paneContainers.right.destroy(); 317 this.paneContainers.bottom.destroy(); 318 319 _.values(this.panelContainers).forEach(panelContainer => { 320 panelContainer.destroy(); 321 }); 322 323 this.paneContainers = { 324 center: this.createCenter(), 325 left: this.createDock('left'), 326 right: this.createDock('right'), 327 bottom: this.createDock('bottom') 328 }; 329 this.activePaneContainer = this.paneContainers.center; 330 this.hasActiveTextEditor = false; 331 332 this.panelContainers = { 333 top: new PanelContainer({ 334 viewRegistry: this.viewRegistry, 335 location: 'top' 336 }), 337 left: new PanelContainer({ 338 viewRegistry: this.viewRegistry, 339 location: 'left', 340 dock: this.paneContainers.left 341 }), 342 right: new PanelContainer({ 343 viewRegistry: this.viewRegistry, 344 location: 'right', 345 dock: this.paneContainers.right 346 }), 347 bottom: new PanelContainer({ 348 viewRegistry: this.viewRegistry, 349 location: 'bottom', 350 dock: this.paneContainers.bottom 351 }), 352 header: new PanelContainer({ 353 viewRegistry: this.viewRegistry, 354 location: 'header' 355 }), 356 footer: new PanelContainer({ 357 viewRegistry: this.viewRegistry, 358 location: 'footer' 359 }), 360 modal: new PanelContainer({ 361 viewRegistry: this.viewRegistry, 362 location: 'modal' 363 }) 364 }; 365 366 this.originalFontSize = null; 367 this.openers = []; 368 this.destroyedItemURIs = []; 369 if (this.element) { 370 this.element.destroy(); 371 this.element = null; 372 } 373 this.consumeServices(this.packageManager); 374 } 375 376 initialize() { 377 this.originalFontSize = this.config.get('editor.fontSize'); 378 this.project.onDidChangePaths(this.updateWindowTitle); 379 this.subscribeToAddedItems(); 380 this.subscribeToMovedItems(); 381 this.subscribeToDockToggling(); 382 } 383 384 consumeServices({ serviceHub }) { 385 this.directorySearchers = []; 386 serviceHub.consume('atom.directory-searcher', '^0.1.0', provider => 387 this.directorySearchers.unshift(provider) 388 ); 389 } 390 391 // Called by the Serializable mixin during serialization. 392 serialize() { 393 return { 394 deserializer: 'Workspace', 395 packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), 396 destroyedItemURIs: this.destroyedItemURIs.slice(), 397 // Ensure deserializing 1.17 state with pre 1.17 Atom does not error 398 // TODO: Remove after 1.17 has been on stable for a while 399 paneContainer: { version: 2 }, 400 paneContainers: { 401 center: this.paneContainers.center.serialize(), 402 left: this.paneContainers.left.serialize(), 403 right: this.paneContainers.right.serialize(), 404 bottom: this.paneContainers.bottom.serialize() 405 } 406 }; 407 } 408 409 deserialize(state, deserializerManager) { 410 const packagesWithActiveGrammars = 411 state.packagesWithActiveGrammars != null 412 ? state.packagesWithActiveGrammars 413 : []; 414 for (let packageName of packagesWithActiveGrammars) { 415 const pkg = this.packageManager.getLoadedPackage(packageName); 416 if (pkg != null) { 417 pkg.loadGrammarsSync(); 418 } 419 } 420 if (state.destroyedItemURIs != null) { 421 this.destroyedItemURIs = state.destroyedItemURIs; 422 } 423 424 if (state.paneContainers) { 425 this.paneContainers.center.deserialize( 426 state.paneContainers.center, 427 deserializerManager 428 ); 429 this.paneContainers.left.deserialize( 430 state.paneContainers.left, 431 deserializerManager 432 ); 433 this.paneContainers.right.deserialize( 434 state.paneContainers.right, 435 deserializerManager 436 ); 437 this.paneContainers.bottom.deserialize( 438 state.paneContainers.bottom, 439 deserializerManager 440 ); 441 } else if (state.paneContainer) { 442 // TODO: Remove this fallback once a lot of time has passed since 1.17 was released 443 this.paneContainers.center.deserialize( 444 state.paneContainer, 445 deserializerManager 446 ); 447 } 448 449 this.hasActiveTextEditor = this.getActiveTextEditor() != null; 450 451 this.updateWindowTitle(); 452 } 453 454 getPackageNamesWithActiveGrammars() { 455 const packageNames = []; 456 const addGrammar = ({ includedGrammarScopes, packageName } = {}) => { 457 if (!packageName) { 458 return; 459 } 460 // Prevent cycles 461 if (packageNames.indexOf(packageName) !== -1) { 462 return; 463 } 464 465 packageNames.push(packageName); 466 for (let scopeName of includedGrammarScopes != null 467 ? includedGrammarScopes 468 : []) { 469 addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)); 470 } 471 }; 472 473 const editors = this.getTextEditors(); 474 for (let editor of editors) { 475 addGrammar(editor.getGrammar()); 476 } 477 478 if (editors.length > 0) { 479 for (let grammar of this.grammarRegistry.getGrammars()) { 480 if (grammar.injectionSelector) { 481 addGrammar(grammar); 482 } 483 } 484 } 485 486 return _.uniq(packageNames); 487 } 488 489 didActivatePaneContainer(paneContainer) { 490 if (paneContainer !== this.getActivePaneContainer()) { 491 this.activePaneContainer = paneContainer; 492 this.didChangeActivePaneItem( 493 this.activePaneContainer.getActivePaneItem() 494 ); 495 this.emitter.emit( 496 'did-change-active-pane-container', 497 this.activePaneContainer 498 ); 499 this.emitter.emit( 500 'did-change-active-pane', 501 this.activePaneContainer.getActivePane() 502 ); 503 this.emitter.emit( 504 'did-change-active-pane-item', 505 this.activePaneContainer.getActivePaneItem() 506 ); 507 } 508 } 509 510 didChangeActivePaneOnPaneContainer(paneContainer, pane) { 511 if (paneContainer === this.getActivePaneContainer()) { 512 this.emitter.emit('did-change-active-pane', pane); 513 } 514 } 515 516 didChangeActivePaneItemOnPaneContainer(paneContainer, item) { 517 if (paneContainer === this.getActivePaneContainer()) { 518 this.didChangeActivePaneItem(item); 519 this.emitter.emit('did-change-active-pane-item', item); 520 } 521 522 if (paneContainer === this.getCenter()) { 523 const hadActiveTextEditor = this.hasActiveTextEditor; 524 this.hasActiveTextEditor = item instanceof TextEditor; 525 526 if (this.hasActiveTextEditor || hadActiveTextEditor) { 527 const itemValue = this.hasActiveTextEditor ? item : undefined; 528 this.emitter.emit('did-change-active-text-editor', itemValue); 529 } 530 } 531 } 532 533 didChangeActivePaneItem(item) { 534 this.updateWindowTitle(); 535 this.updateDocumentEdited(); 536 if (this.activeItemSubscriptions) this.activeItemSubscriptions.dispose(); 537 this.activeItemSubscriptions = new CompositeDisposable(); 538 539 let modifiedSubscription, titleSubscription; 540 541 if (item != null && typeof item.onDidChangeTitle === 'function') { 542 titleSubscription = item.onDidChangeTitle(this.updateWindowTitle); 543 } else if (item != null && typeof item.on === 'function') { 544 titleSubscription = item.on('title-changed', this.updateWindowTitle); 545 if ( 546 titleSubscription == null || 547 typeof titleSubscription.dispose !== 'function' 548 ) { 549 titleSubscription = new Disposable(() => { 550 item.off('title-changed', this.updateWindowTitle); 551 }); 552 } 553 } 554 555 if (item != null && typeof item.onDidChangeModified === 'function') { 556 modifiedSubscription = item.onDidChangeModified( 557 this.updateDocumentEdited 558 ); 559 } else if (item != null && typeof item.on === 'function') { 560 modifiedSubscription = item.on( 561 'modified-status-changed', 562 this.updateDocumentEdited 563 ); 564 if ( 565 modifiedSubscription == null || 566 typeof modifiedSubscription.dispose !== 'function' 567 ) { 568 modifiedSubscription = new Disposable(() => { 569 item.off('modified-status-changed', this.updateDocumentEdited); 570 }); 571 } 572 } 573 574 if (titleSubscription != null) { 575 this.activeItemSubscriptions.add(titleSubscription); 576 } 577 if (modifiedSubscription != null) { 578 this.activeItemSubscriptions.add(modifiedSubscription); 579 } 580 581 this.cancelStoppedChangingActivePaneItemTimeout(); 582 this.stoppedChangingActivePaneItemTimeout = setTimeout(() => { 583 this.stoppedChangingActivePaneItemTimeout = null; 584 this.emitter.emit('did-stop-changing-active-pane-item', item); 585 }, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY); 586 } 587 588 cancelStoppedChangingActivePaneItemTimeout() { 589 if (this.stoppedChangingActivePaneItemTimeout != null) { 590 clearTimeout(this.stoppedChangingActivePaneItemTimeout); 591 } 592 } 593 594 setDraggingItem(draggingItem) { 595 _.values(this.paneContainers).forEach(dock => { 596 dock.setDraggingItem(draggingItem); 597 }); 598 } 599 600 subscribeToAddedItems() { 601 this.onDidAddPaneItem(({ item, pane, index }) => { 602 if (item instanceof TextEditor) { 603 const subscriptions = new CompositeDisposable( 604 this.textEditorRegistry.add(item), 605 this.textEditorRegistry.maintainConfig(item) 606 ); 607 if (!this.project.findBufferForId(item.buffer.id)) { 608 this.project.addBuffer(item.buffer); 609 } 610 item.onDidDestroy(() => { 611 subscriptions.dispose(); 612 }); 613 this.emitter.emit('did-add-text-editor', { 614 textEditor: item, 615 pane, 616 index 617 }); 618 // It's important to call handleGrammarUsed after emitting the did-add event: 619 // if we activate a package between adding the editor to the registry and emitting 620 // the package may receive the editor twice from `observeTextEditors`. 621 // (Note that the item can be destroyed by an `observeTextEditors` handler.) 622 if (!item.isDestroyed()) { 623 subscriptions.add( 624 item.observeGrammar(this.handleGrammarUsed.bind(this)) 625 ); 626 } 627 } 628 }); 629 } 630 631 subscribeToDockToggling() { 632 const docks = [ 633 this.getLeftDock(), 634 this.getRightDock(), 635 this.getBottomDock() 636 ]; 637 docks.forEach(dock => { 638 dock.onDidChangeVisible(visible => { 639 if (visible) return; 640 const { activeElement } = document; 641 const dockElement = dock.getElement(); 642 if ( 643 dockElement === activeElement || 644 dockElement.contains(activeElement) 645 ) { 646 this.getCenter().activate(); 647 } 648 }); 649 }); 650 } 651 652 subscribeToMovedItems() { 653 for (const paneContainer of this.getPaneContainers()) { 654 paneContainer.observePanes(pane => { 655 pane.onDidAddItem(({ item }) => { 656 if (typeof item.getURI === 'function' && this.enablePersistence) { 657 const uri = item.getURI(); 658 if (uri) { 659 const location = paneContainer.getLocation(); 660 let defaultLocation; 661 if (typeof item.getDefaultLocation === 'function') { 662 defaultLocation = item.getDefaultLocation(); 663 } 664 defaultLocation = defaultLocation || 'center'; 665 if (location === defaultLocation) { 666 this.itemLocationStore.delete(item.getURI()); 667 } else { 668 this.itemLocationStore.save(item.getURI(), location); 669 } 670 } 671 } 672 }); 673 }); 674 } 675 } 676 677 // Updates the application's title and proxy icon based on whichever file is 678 // open. 679 updateWindowTitle() { 680 let itemPath, itemTitle, projectPath, representedPath; 681 const appName = atom.getAppName(); 682 const left = this.project.getPaths(); 683 const projectPaths = left != null ? left : []; 684 const item = this.getActivePaneItem(); 685 if (item) { 686 itemPath = 687 typeof item.getPath === 'function' ? item.getPath() : undefined; 688 const longTitle = 689 typeof item.getLongTitle === 'function' 690 ? item.getLongTitle() 691 : undefined; 692 itemTitle = 693 longTitle == null 694 ? typeof item.getTitle === 'function' 695 ? item.getTitle() 696 : undefined 697 : longTitle; 698 projectPath = _.find( 699 projectPaths, 700 projectPath => 701 itemPath === projectPath || 702 (itemPath != null 703 ? itemPath.startsWith(projectPath + path.sep) 704 : undefined) 705 ); 706 } 707 if (itemTitle == null) { 708 itemTitle = 'untitled'; 709 } 710 if (projectPath == null) { 711 projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0]; 712 } 713 if (projectPath != null) { 714 projectPath = fs.tildify(projectPath); 715 } 716 717 const titleParts = []; 718 if (item != null && projectPath != null) { 719 titleParts.push(itemTitle, projectPath); 720 representedPath = itemPath != null ? itemPath : projectPath; 721 } else if (projectPath != null) { 722 titleParts.push(projectPath); 723 representedPath = projectPath; 724 } else { 725 titleParts.push(itemTitle); 726 representedPath = ''; 727 } 728 729 if (process.platform !== 'darwin') { 730 titleParts.push(appName); 731 } 732 733 document.title = titleParts.join(' \u2014 '); 734 this.applicationDelegate.setRepresentedFilename(representedPath); 735 this.emitter.emit('did-change-window-title'); 736 } 737 738 // On macOS, fades the application window's proxy icon when the current file 739 // has been modified. 740 updateDocumentEdited() { 741 const activePaneItem = this.getActivePaneItem(); 742 const modified = 743 activePaneItem != null && typeof activePaneItem.isModified === 'function' 744 ? activePaneItem.isModified() || false 745 : false; 746 this.applicationDelegate.setWindowDocumentEdited(modified); 747 } 748 749 /* 750 Section: Event Subscription 751 */ 752 753 onDidChangeActivePaneContainer(callback) { 754 return this.emitter.on('did-change-active-pane-container', callback); 755 } 756 757 // Essential: Invoke the given callback with all current and future text 758 // editors in the workspace. 759 // 760 // * `callback` {Function} to be called with current and future text editors. 761 // * `editor` A {TextEditor} that is present in {::getTextEditors} at the time 762 // of subscription or that is added at some later time. 763 // 764 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 765 observeTextEditors(callback) { 766 for (let textEditor of this.getTextEditors()) { 767 callback(textEditor); 768 } 769 return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor)); 770 } 771 772 // Essential: Invoke the given callback with all current and future panes items 773 // in the workspace. 774 // 775 // * `callback` {Function} to be called with current and future pane items. 776 // * `item` An item that is present in {::getPaneItems} at the time of 777 // subscription or that is added at some later time. 778 // 779 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 780 observePaneItems(callback) { 781 return new CompositeDisposable( 782 ...this.getPaneContainers().map(container => 783 container.observePaneItems(callback) 784 ) 785 ); 786 } 787 788 // Essential: Invoke the given callback when the active pane item changes. 789 // 790 // Because observers are invoked synchronously, it's important not to perform 791 // any expensive operations via this method. Consider 792 // {::onDidStopChangingActivePaneItem} to delay operations until after changes 793 // stop occurring. 794 // 795 // * `callback` {Function} to be called when the active pane item changes. 796 // * `item` The active pane item. 797 // 798 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 799 onDidChangeActivePaneItem(callback) { 800 return this.emitter.on('did-change-active-pane-item', callback); 801 } 802 803 // Essential: Invoke the given callback when the active pane item stops 804 // changing. 805 // 806 // Observers are called asynchronously 100ms after the last active pane item 807 // change. Handling changes here rather than in the synchronous 808 // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly 809 // changing or closing tabs and ensures critical UI feedback, like changing the 810 // highlighted tab, gets priority over work that can be done asynchronously. 811 // 812 // * `callback` {Function} to be called when the active pane item stops 813 // changing. 814 // * `item` The active pane item. 815 // 816 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 817 onDidStopChangingActivePaneItem(callback) { 818 return this.emitter.on('did-stop-changing-active-pane-item', callback); 819 } 820 821 // Essential: Invoke the given callback when a text editor becomes the active 822 // text editor and when there is no longer an active text editor. 823 // 824 // * `callback` {Function} to be called when the active text editor changes. 825 // * `editor` The active {TextEditor} or undefined if there is no longer an 826 // active text editor. 827 // 828 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 829 onDidChangeActiveTextEditor(callback) { 830 return this.emitter.on('did-change-active-text-editor', callback); 831 } 832 833 // Essential: Invoke the given callback with the current active pane item and 834 // with all future active pane items in the workspace. 835 // 836 // * `callback` {Function} to be called when the active pane item changes. 837 // * `item` The current active pane item. 838 // 839 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 840 observeActivePaneItem(callback) { 841 callback(this.getActivePaneItem()); 842 return this.onDidChangeActivePaneItem(callback); 843 } 844 845 // Essential: Invoke the given callback with the current active text editor 846 // (if any), with all future active text editors, and when there is no longer 847 // an active text editor. 848 // 849 // * `callback` {Function} to be called when the active text editor changes. 850 // * `editor` The active {TextEditor} or undefined if there is not an 851 // active text editor. 852 // 853 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 854 observeActiveTextEditor(callback) { 855 callback(this.getActiveTextEditor()); 856 857 return this.onDidChangeActiveTextEditor(callback); 858 } 859 860 // Essential: Invoke the given callback whenever an item is opened. Unlike 861 // {::onDidAddPaneItem}, observers will be notified for items that are already 862 // present in the workspace when they are reopened. 863 // 864 // * `callback` {Function} to be called whenever an item is opened. 865 // * `event` {Object} with the following keys: 866 // * `uri` {String} representing the opened URI. Could be `undefined`. 867 // * `item` The opened item. 868 // * `pane` The pane in which the item was opened. 869 // * `index` The index of the opened item on its pane. 870 // 871 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 872 onDidOpen(callback) { 873 return this.emitter.on('did-open', callback); 874 } 875 876 // Extended: Invoke the given callback when a pane is added to the workspace. 877 // 878 // * `callback` {Function} to be called panes are added. 879 // * `event` {Object} with the following keys: 880 // * `pane` The added pane. 881 // 882 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 883 onDidAddPane(callback) { 884 return new CompositeDisposable( 885 ...this.getPaneContainers().map(container => 886 container.onDidAddPane(callback) 887 ) 888 ); 889 } 890 891 // Extended: Invoke the given callback before a pane is destroyed in the 892 // workspace. 893 // 894 // * `callback` {Function} to be called before panes are destroyed. 895 // * `event` {Object} with the following keys: 896 // * `pane` The pane to be destroyed. 897 // 898 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 899 onWillDestroyPane(callback) { 900 return new CompositeDisposable( 901 ...this.getPaneContainers().map(container => 902 container.onWillDestroyPane(callback) 903 ) 904 ); 905 } 906 907 // Extended: Invoke the given callback when a pane is destroyed in the 908 // workspace. 909 // 910 // * `callback` {Function} to be called panes are destroyed. 911 // * `event` {Object} with the following keys: 912 // * `pane` The destroyed pane. 913 // 914 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 915 onDidDestroyPane(callback) { 916 return new CompositeDisposable( 917 ...this.getPaneContainers().map(container => 918 container.onDidDestroyPane(callback) 919 ) 920 ); 921 } 922 923 // Extended: Invoke the given callback with all current and future panes in the 924 // workspace. 925 // 926 // * `callback` {Function} to be called with current and future panes. 927 // * `pane` A {Pane} that is present in {::getPanes} at the time of 928 // subscription or that is added at some later time. 929 // 930 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 931 observePanes(callback) { 932 return new CompositeDisposable( 933 ...this.getPaneContainers().map(container => 934 container.observePanes(callback) 935 ) 936 ); 937 } 938 939 // Extended: Invoke the given callback when the active pane changes. 940 // 941 // * `callback` {Function} to be called when the active pane changes. 942 // * `pane` A {Pane} that is the current return value of {::getActivePane}. 943 // 944 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 945 onDidChangeActivePane(callback) { 946 return this.emitter.on('did-change-active-pane', callback); 947 } 948 949 // Extended: Invoke the given callback with the current active pane and when 950 // the active pane changes. 951 // 952 // * `callback` {Function} to be called with the current and future active# 953 // panes. 954 // * `pane` A {Pane} that is the current return value of {::getActivePane}. 955 // 956 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 957 observeActivePane(callback) { 958 callback(this.getActivePane()); 959 return this.onDidChangeActivePane(callback); 960 } 961 962 // Extended: Invoke the given callback when a pane item is added to the 963 // workspace. 964 // 965 // * `callback` {Function} to be called when pane items are added. 966 // * `event` {Object} with the following keys: 967 // * `item` The added pane item. 968 // * `pane` {Pane} containing the added item. 969 // * `index` {Number} indicating the index of the added item in its pane. 970 // 971 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 972 onDidAddPaneItem(callback) { 973 return new CompositeDisposable( 974 ...this.getPaneContainers().map(container => 975 container.onDidAddPaneItem(callback) 976 ) 977 ); 978 } 979 980 // Extended: Invoke the given callback when a pane item is about to be 981 // destroyed, before the user is prompted to save it. 982 // 983 // * `callback` {Function} to be called before pane items are destroyed. If this function returns 984 // a {Promise}, then the item will not be destroyed until the promise resolves. 985 // * `event` {Object} with the following keys: 986 // * `item` The item to be destroyed. 987 // * `pane` {Pane} containing the item to be destroyed. 988 // * `index` {Number} indicating the index of the item to be destroyed in 989 // its pane. 990 // 991 // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. 992 onWillDestroyPaneItem(callback) { 993 return new CompositeDisposable( 994 ...this.getPaneContainers().map(container => 995 container.onWillDestroyPaneItem(callback) 996 ) 997 ); 998 } 999 1000 // Extended: Invoke the given callback when a pane item is destroyed. 1001 // 1002 // * `callback` {Function} to be called when pane items are destroyed. 1003 // * `event` {Object} with the following keys: 1004 // * `item` The destroyed item. 1005 // * `pane` {Pane} containing the destroyed item. 1006 // * `index` {Number} indicating the index of the destroyed item in its 1007 // pane. 1008 // 1009 // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. 1010 onDidDestroyPaneItem(callback) { 1011 return new CompositeDisposable( 1012 ...this.getPaneContainers().map(container => 1013 container.onDidDestroyPaneItem(callback) 1014 ) 1015 ); 1016 } 1017 1018 // Extended: Invoke the given callback when a text editor is added to the 1019 // workspace. 1020 // 1021 // * `callback` {Function} to be called panes are added. 1022 // * `event` {Object} with the following keys: 1023 // * `textEditor` {TextEditor} that was added. 1024 // * `pane` {Pane} containing the added text editor. 1025 // * `index` {Number} indicating the index of the added text editor in its 1026 // pane. 1027 // 1028 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 1029 onDidAddTextEditor(callback) { 1030 return this.emitter.on('did-add-text-editor', callback); 1031 } 1032 1033 onDidChangeWindowTitle(callback) { 1034 return this.emitter.on('did-change-window-title', callback); 1035 } 1036 1037 /* 1038 Section: Opening 1039 */ 1040 1041 // Essential: Opens the given URI in Atom asynchronously. 1042 // If the URI is already open, the existing item for that URI will be 1043 // activated. If no URI is given, or no registered opener can open 1044 // the URI, a new empty {TextEditor} will be created. 1045 // 1046 // * `uri` (optional) A {String} containing a URI. 1047 // * `options` (optional) {Object} 1048 // * `initialLine` A {Number} indicating which row to move the cursor to 1049 // initially. Defaults to `0`. 1050 // * `initialColumn` A {Number} indicating which column to move the cursor to 1051 // initially. Defaults to `0`. 1052 // * `split` Either 'left', 'right', 'up' or 'down'. 1053 // If 'left', the item will be opened in leftmost pane of the current active pane's row. 1054 // If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. 1055 // If 'up', the item will be opened in topmost pane of the current active pane's column. 1056 // If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. 1057 // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on 1058 // containing pane. Defaults to `true`. 1059 // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} 1060 // on containing pane. Defaults to `true`. 1061 // * `pending` A {Boolean} indicating whether or not the item should be opened 1062 // in a pending state. Existing pending items in a pane are replaced with 1063 // new pending items when they are opened. 1064 // * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to 1065 // activate an existing item for the given URI on any pane. 1066 // If `false`, only the active pane will be searched for 1067 // an existing item for the same URI. Defaults to `false`. 1068 // * `location` (optional) A {String} containing the name of the location 1069 // in which this item should be opened (one of "left", "right", "bottom", 1070 // or "center"). If omitted, Atom will fall back to the last location in 1071 // which a user has placed an item with the same URI or, if this is a new 1072 // URI, the default location specified by the item. NOTE: This option 1073 // should almost always be omitted to honor user preference. 1074 // 1075 // Returns a {Promise} that resolves to the {TextEditor} for the file URI. 1076 async open(itemOrURI, options = {}) { 1077 let uri, item; 1078 if (typeof itemOrURI === 'string') { 1079 uri = this.project.resolvePath(itemOrURI); 1080 } else if (itemOrURI) { 1081 item = itemOrURI; 1082 if (typeof item.getURI === 'function') uri = item.getURI(); 1083 } 1084 1085 let resolveItem = () => {}; 1086 if (uri) { 1087 const incomingItem = this.incoming.get(uri); 1088 if (!incomingItem) { 1089 this.incoming.set( 1090 uri, 1091 new Promise(resolve => { 1092 resolveItem = resolve; 1093 }) 1094 ); 1095 } else { 1096 await incomingItem; 1097 } 1098 } 1099 1100 try { 1101 if (!atom.config.get('core.allowPendingPaneItems')) { 1102 options.pending = false; 1103 } 1104 1105 // Avoid adding URLs as recent documents to work-around this Spotlight crash: 1106 // https://github.com/atom/atom/issues/10071 1107 if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) { 1108 this.applicationDelegate.addRecentDocument(uri); 1109 } 1110 1111 let pane, itemExistsInWorkspace; 1112 1113 // Try to find an existing item in the workspace. 1114 if (item || uri) { 1115 if (options.pane) { 1116 pane = options.pane; 1117 } else if (options.searchAllPanes) { 1118 pane = item ? this.paneForItem(item) : this.paneForURI(uri); 1119 } else { 1120 // If an item with the given URI is already in the workspace, assume 1121 // that item's pane container is the preferred location for that URI. 1122 let container; 1123 if (uri) container = this.paneContainerForURI(uri); 1124 if (!container) container = this.getActivePaneContainer(); 1125 1126 // The `split` option affects where we search for the item. 1127 pane = container.getActivePane(); 1128 switch (options.split) { 1129 case 'left': 1130 pane = pane.findLeftmostSibling(); 1131 break; 1132 case 'right': 1133 pane = pane.findRightmostSibling(); 1134 break; 1135 case 'up': 1136 pane = pane.findTopmostSibling(); 1137 break; 1138 case 'down': 1139 pane = pane.findBottommostSibling(); 1140 break; 1141 } 1142 } 1143 1144 if (pane) { 1145 if (item) { 1146 itemExistsInWorkspace = pane.getItems().includes(item); 1147 } else { 1148 item = pane.itemForURI(uri); 1149 itemExistsInWorkspace = item != null; 1150 } 1151 } 1152 } 1153 1154 // If we already have an item at this stage, we won't need to do an async 1155 // lookup of the URI, so we yield the event loop to ensure this method 1156 // is consistently asynchronous. 1157 if (item) await Promise.resolve(); 1158 1159 if (!itemExistsInWorkspace) { 1160 item = item || (await this.createItemForURI(uri, options)); 1161 if (!item) return; 1162 1163 if (options.pane) { 1164 pane = options.pane; 1165 } else { 1166 let location = options.location; 1167 if (!location && !options.split && uri && this.enablePersistence) { 1168 location = await this.itemLocationStore.load(uri); 1169 } 1170 if (!location && typeof item.getDefaultLocation === 'function') { 1171 location = item.getDefaultLocation(); 1172 } 1173 1174 const allowedLocations = 1175 typeof item.getAllowedLocations === 'function' 1176 ? item.getAllowedLocations() 1177 : ALL_LOCATIONS; 1178 location = allowedLocations.includes(location) 1179 ? location 1180 : allowedLocations[0]; 1181 1182 const container = this.paneContainers[location] || this.getCenter(); 1183 pane = container.getActivePane(); 1184 switch (options.split) { 1185 case 'left': 1186 pane = pane.findLeftmostSibling(); 1187 break; 1188 case 'right': 1189 pane = pane.findOrCreateRightmostSibling(); 1190 break; 1191 case 'up': 1192 pane = pane.findTopmostSibling(); 1193 break; 1194 case 'down': 1195 pane = pane.findOrCreateBottommostSibling(); 1196 break; 1197 } 1198 } 1199 } 1200 1201 if (!options.pending && pane.getPendingItem() === item) { 1202 pane.clearPendingItem(); 1203 } 1204 1205 this.itemOpened(item); 1206 1207 if (options.activateItem === false) { 1208 pane.addItem(item, { pending: options.pending }); 1209 } else { 1210 pane.activateItem(item, { pending: options.pending }); 1211 } 1212 1213 if (options.activatePane !== false) { 1214 pane.activate(); 1215 } 1216 1217 let initialColumn = 0; 1218 let initialLine = 0; 1219 if (!Number.isNaN(options.initialLine)) { 1220 initialLine = options.initialLine; 1221 } 1222 if (!Number.isNaN(options.initialColumn)) { 1223 initialColumn = options.initialColumn; 1224 } 1225 if (initialLine >= 0 || initialColumn >= 0) { 1226 if (typeof item.setCursorBufferPosition === 'function') { 1227 item.setCursorBufferPosition([initialLine, initialColumn]); 1228 } 1229 if (typeof item.unfoldBufferRow === 'function') { 1230 item.unfoldBufferRow(initialLine); 1231 } 1232 if (typeof item.scrollToBufferPosition === 'function') { 1233 item.scrollToBufferPosition([initialLine, initialColumn], { 1234 center: true 1235 }); 1236 } 1237 } 1238 1239 const index = pane.getActiveItemIndex(); 1240 this.emitter.emit('did-open', { uri, pane, item, index }); 1241 if (uri) { 1242 this.incoming.delete(uri); 1243 } 1244 } finally { 1245 resolveItem(); 1246 } 1247 return item; 1248 } 1249 1250 // Essential: Search the workspace for items matching the given URI and hide them. 1251 // 1252 // * `itemOrURI` The item to hide or a {String} containing the URI 1253 // of the item to hide. 1254 // 1255 // Returns a {Boolean} indicating whether any items were found (and hidden). 1256 hide(itemOrURI) { 1257 let foundItems = false; 1258 1259 // If any visible item has the given URI, hide it 1260 for (const container of this.getPaneContainers()) { 1261 const isCenter = container === this.getCenter(); 1262 if (isCenter || container.isVisible()) { 1263 for (const pane of container.getPanes()) { 1264 const activeItem = pane.getActiveItem(); 1265 const foundItem = 1266 activeItem != null && 1267 (activeItem === itemOrURI || 1268 (typeof activeItem.getURI === 'function' && 1269 activeItem.getURI() === itemOrURI)); 1270 if (foundItem) { 1271 foundItems = true; 1272 // We can't really hide the center so we just destroy the item. 1273 if (isCenter) { 1274 pane.destroyItem(activeItem); 1275 } else { 1276 container.hide(); 1277 } 1278 } 1279 } 1280 } 1281 } 1282 1283 return foundItems; 1284 } 1285 1286 // Essential: Search the workspace for items matching the given URI. If any are found, hide them. 1287 // Otherwise, open the URL. 1288 // 1289 // * `itemOrURI` (optional) The item to toggle or a {String} containing the URI 1290 // of the item to toggle. 1291 // 1292 // Returns a Promise that resolves when the item is shown or hidden. 1293 toggle(itemOrURI) { 1294 if (this.hide(itemOrURI)) { 1295 return Promise.resolve(); 1296 } else { 1297 return this.open(itemOrURI, { searchAllPanes: true }); 1298 } 1299 } 1300 1301 // Open Atom's license in the active pane. 1302 openLicense() { 1303 return this.open(path.join(process.resourcesPath, 'LICENSE.md')); 1304 } 1305 1306 // Synchronously open the given URI in the active pane. **Only use this method 1307 // in specs. Calling this in production code will block the UI thread and 1308 // everyone will be mad at you.** 1309 // 1310 // * `uri` A {String} containing a URI. 1311 // * `options` An optional options {Object} 1312 // * `initialLine` A {Number} indicating which row to move the cursor to 1313 // initially. Defaults to `0`. 1314 // * `initialColumn` A {Number} indicating which column to move the cursor to 1315 // initially. Defaults to `0`. 1316 // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on 1317 // the containing pane. Defaults to `true`. 1318 // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} 1319 // on containing pane. Defaults to `true`. 1320 openSync(uri_ = '', options = {}) { 1321 const { initialLine, initialColumn } = options; 1322 const activatePane = 1323 options.activatePane != null ? options.activatePane : true; 1324 const activateItem = 1325 options.activateItem != null ? options.activateItem : true; 1326 1327 const uri = this.project.resolvePath(uri_); 1328 let item = this.getActivePane().itemForURI(uri); 1329 if (uri && item == null) { 1330 for (const opener of this.getOpeners()) { 1331 item = opener(uri, options); 1332 if (item) break; 1333 } 1334 } 1335 if (item == null) { 1336 item = this.project.openSync(uri, { initialLine, initialColumn }); 1337 } 1338 1339 if (activateItem) { 1340 this.getActivePane().activateItem(item); 1341 } 1342 this.itemOpened(item); 1343 if (activatePane) { 1344 this.getActivePane().activate(); 1345 } 1346 return item; 1347 } 1348 1349 openURIInPane(uri, pane) { 1350 return this.open(uri, { pane }); 1351 } 1352 1353 // Public: Creates a new item that corresponds to the provided URI. 1354 // 1355 // If no URI is given, or no registered opener can open the URI, a new empty 1356 // {TextEditor} will be created. 1357 // 1358 // * `uri` A {String} containing a URI. 1359 // 1360 // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI. 1361 async createItemForURI(uri, options) { 1362 if (uri != null) { 1363 for (const opener of this.getOpeners()) { 1364 const item = opener(uri, options); 1365 if (item != null) return item; 1366 } 1367 } 1368 1369 try { 1370 const item = await this.openTextFile(uri, options); 1371 return item; 1372 } catch (error) { 1373 switch (error.code) { 1374 case 'CANCELLED': 1375 return Promise.resolve(); 1376 case 'EACCES': 1377 this.notificationManager.addWarning( 1378 `Permission denied '${error.path}'` 1379 ); 1380 return Promise.resolve(); 1381 case 'EPERM': 1382 case 'EBUSY': 1383 case 'ENXIO': 1384 case 'EIO': 1385 case 'ENOTCONN': 1386 case 'UNKNOWN': 1387 case 'ECONNRESET': 1388 case 'EINVAL': 1389 case 'EMFILE': 1390 case 'ENOTDIR': 1391 case 'EAGAIN': 1392 this.notificationManager.addWarning( 1393 `Unable to open '${error.path != null ? error.path : uri}'`, 1394 { detail: error.message } 1395 ); 1396 return Promise.resolve(); 1397 default: 1398 throw error; 1399 } 1400 } 1401 } 1402 1403 async openTextFile(uri, options) { 1404 const filePath = this.project.resolvePath(uri); 1405 1406 if (filePath != null) { 1407 try { 1408 fs.closeSync(fs.openSync(filePath, 'r')); 1409 } catch (error) { 1410 // allow ENOENT errors to create an editor for paths that dont exist 1411 if (error.code !== 'ENOENT') { 1412 throw error; 1413 } 1414 } 1415 } 1416 1417 const fileSize = fs.getSizeSync(filePath); 1418 1419 if (fileSize >= this.config.get('core.warnOnLargeFileLimit') * 1048576) { 1420 // 40MB by default 1421 await new Promise((resolve, reject) => { 1422 this.applicationDelegate.confirm( 1423 { 1424 message: 1425 'Atom will be unresponsive during the loading of very large files.', 1426 detail: 'Do you still want to load this file?', 1427 buttons: ['Proceed', 'Cancel'] 1428 }, 1429 response => { 1430 if (response === 1) { 1431 const error = new Error(); 1432 error.code = 'CANCELLED'; 1433 reject(error); 1434 } else { 1435 resolve(); 1436 } 1437 } 1438 ); 1439 }); 1440 } 1441 1442 const buffer = await this.project.bufferForPath(filePath, options); 1443 return this.textEditorRegistry.build( 1444 Object.assign({ buffer, autoHeight: false }, options) 1445 ); 1446 } 1447 1448 handleGrammarUsed(grammar) { 1449 if (grammar == null) { 1450 return; 1451 } 1452 this.packageManager.triggerActivationHook( 1453 `${grammar.scopeName}:root-scope-used` 1454 ); 1455 this.packageManager.triggerActivationHook( 1456 `${grammar.packageName}:grammar-used` 1457 ); 1458 } 1459 1460 // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. 1461 // 1462 // * `object` An {Object} you want to perform the check against. 1463 isTextEditor(object) { 1464 return object instanceof TextEditor; 1465 } 1466 1467 // Extended: Create a new text editor. 1468 // 1469 // Returns a {TextEditor}. 1470 buildTextEditor(params) { 1471 const editor = this.textEditorRegistry.build(params); 1472 const subscription = this.textEditorRegistry.maintainConfig(editor); 1473 editor.onDidDestroy(() => subscription.dispose()); 1474 return editor; 1475 } 1476 1477 // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been 1478 // reopened. 1479 // 1480 // Returns a {Promise} that is resolved when the item is opened 1481 reopenItem() { 1482 const uri = this.destroyedItemURIs.pop(); 1483 if (uri) { 1484 return this.open(uri); 1485 } else { 1486 return Promise.resolve(); 1487 } 1488 } 1489 1490 // Public: Register an opener for a uri. 1491 // 1492 // When a URI is opened via {Workspace::open}, Atom loops through its registered 1493 // opener functions until one returns a value for the given uri. 1494 // Openers are expected to return an object that inherits from HTMLElement or 1495 // a model which has an associated view in the {ViewRegistry}. 1496 // A {TextEditor} will be used if no opener returns a value. 1497 // 1498 // ## Examples 1499 // 1500 // ```coffee 1501 // atom.workspace.addOpener (uri) -> 1502 // if path.extname(uri) is '.toml' 1503 // return new TomlEditor(uri) 1504 // ``` 1505 // 1506 // * `opener` A {Function} to be called when a path is being opened. 1507 // 1508 // Returns a {Disposable} on which `.dispose()` can be called to remove the 1509 // opener. 1510 // 1511 // Note that the opener will be called if and only if the URI is not already open 1512 // in the current pane. The searchAllPanes flag expands the search from the 1513 // current pane to all panes. If you wish to open a view of a different type for 1514 // a file that is already open, consider changing the protocol of the URI. For 1515 // example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` 1516 // that is already open in a text editor view. You could signal this by calling 1517 // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener 1518 // can check the protocol for quux-preview and only handle those URIs that match. 1519 // 1520 // To defer your package's activation until a specific URL is opened, add a 1521 // `workspaceOpeners` field to your `package.json` containing an array of URL 1522 // strings. 1523 addOpener(opener) { 1524 this.openers.push(opener); 1525 return new Disposable(() => { 1526 _.remove(this.openers, opener); 1527 }); 1528 } 1529 1530 getOpeners() { 1531 return this.openers; 1532 } 1533 1534 /* 1535 Section: Pane Items 1536 */ 1537 1538 // Essential: Get all pane items in the workspace. 1539 // 1540 // Returns an {Array} of items. 1541 getPaneItems() { 1542 return _.flatten( 1543 this.getPaneContainers().map(container => container.getPaneItems()) 1544 ); 1545 } 1546 1547 // Essential: Get the active {Pane}'s active item. 1548 // 1549 // Returns an pane item {Object}. 1550 getActivePaneItem() { 1551 return this.getActivePaneContainer().getActivePaneItem(); 1552 } 1553 1554 // Essential: Get all text editors in the workspace. 1555 // 1556 // Returns an {Array} of {TextEditor}s. 1557 getTextEditors() { 1558 return this.getPaneItems().filter(item => item instanceof TextEditor); 1559 } 1560 1561 // Essential: Get the workspace center's active item if it is a {TextEditor}. 1562 // 1563 // Returns a {TextEditor} or `undefined` if the workspace center's current 1564 // active item is not a {TextEditor}. 1565 getActiveTextEditor() { 1566 const activeItem = this.getCenter().getActivePaneItem(); 1567 if (activeItem instanceof TextEditor) { 1568 return activeItem; 1569 } 1570 } 1571 1572 // Save all pane items. 1573 saveAll() { 1574 this.getPaneContainers().forEach(container => { 1575 container.saveAll(); 1576 }); 1577 } 1578 1579 confirmClose(options) { 1580 return Promise.all( 1581 this.getPaneContainers().map(container => container.confirmClose(options)) 1582 ).then(results => !results.includes(false)); 1583 } 1584 1585 // Save the active pane item. 1586 // 1587 // If the active pane item currently has a URI according to the item's 1588 // `.getURI` method, calls `.save` on the item. Otherwise 1589 // {::saveActivePaneItemAs} # will be called instead. This method does nothing 1590 // if the active item does not implement a `.save` method. 1591 saveActivePaneItem() { 1592 return this.getCenter() 1593 .getActivePane() 1594 .saveActiveItem(); 1595 } 1596 1597 // Prompt the user for a path and save the active pane item to it. 1598 // 1599 // Opens a native dialog where the user selects a path on disk, then calls 1600 // `.saveAs` on the item with the selected path. This method does nothing if 1601 // the active item does not implement a `.saveAs` method. 1602 saveActivePaneItemAs() { 1603 this.getCenter() 1604 .getActivePane() 1605 .saveActiveItemAs(); 1606 } 1607 1608 // Destroy (close) the active pane item. 1609 // 1610 // Removes the active pane item and calls the `.destroy` method on it if one is 1611 // defined. 1612 destroyActivePaneItem() { 1613 return this.getActivePane().destroyActiveItem(); 1614 } 1615 1616 /* 1617 Section: Panes 1618 */ 1619 1620 // Extended: Get the most recently focused pane container. 1621 // 1622 // Returns a {Dock} or the {WorkspaceCenter}. 1623 getActivePaneContainer() { 1624 return this.activePaneContainer; 1625 } 1626 1627 // Extended: Get all panes in the workspace. 1628 // 1629 // Returns an {Array} of {Pane}s. 1630 getPanes() { 1631 return _.flatten( 1632 this.getPaneContainers().map(container => container.getPanes()) 1633 ); 1634 } 1635 1636 getVisiblePanes() { 1637 return _.flatten( 1638 this.getVisiblePaneContainers().map(container => container.getPanes()) 1639 ); 1640 } 1641 1642 // Extended: Get the active {Pane}. 1643 // 1644 // Returns a {Pane}. 1645 getActivePane() { 1646 return this.getActivePaneContainer().getActivePane(); 1647 } 1648 1649 // Extended: Make the next pane active. 1650 activateNextPane() { 1651 return this.getActivePaneContainer().activateNextPane(); 1652 } 1653 1654 // Extended: Make the previous pane active. 1655 activatePreviousPane() { 1656 return this.getActivePaneContainer().activatePreviousPane(); 1657 } 1658 1659 // Extended: Get the first pane container that contains an item with the given 1660 // URI. 1661 // 1662 // * `uri` {String} uri 1663 // 1664 // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists 1665 // with the given URI. 1666 paneContainerForURI(uri) { 1667 return this.getPaneContainers().find(container => 1668 container.paneForURI(uri) 1669 ); 1670 } 1671 1672 // Extended: Get the first pane container that contains the given item. 1673 // 1674 // * `item` the Item that the returned pane container must contain. 1675 // 1676 // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists 1677 // with the given URI. 1678 paneContainerForItem(uri) { 1679 return this.getPaneContainers().find(container => 1680 container.paneForItem(uri) 1681 ); 1682 } 1683 1684 // Extended: Get the first {Pane} that contains an item with the given URI. 1685 // 1686 // * `uri` {String} uri 1687 // 1688 // Returns a {Pane} or `undefined` if no item exists with the given URI. 1689 paneForURI(uri) { 1690 for (let location of this.getPaneContainers()) { 1691 const pane = location.paneForURI(uri); 1692 if (pane != null) { 1693 return pane; 1694 } 1695 } 1696 } 1697 1698 // Extended: Get the {Pane} containing the given item. 1699 // 1700 // * `item` the Item that the returned pane must contain. 1701 // 1702 // Returns a {Pane} or `undefined` if no pane exists for the given item. 1703 paneForItem(item) { 1704 for (let location of this.getPaneContainers()) { 1705 const pane = location.paneForItem(item); 1706 if (pane != null) { 1707 return pane; 1708 } 1709 } 1710 } 1711 1712 // Destroy (close) the active pane. 1713 destroyActivePane() { 1714 const activePane = this.getActivePane(); 1715 if (activePane != null) { 1716 activePane.destroy(); 1717 } 1718 } 1719 1720 // Close the active center pane item, or the active center pane if it is 1721 // empty, or the current window if there is only the empty root pane. 1722 closeActivePaneItemOrEmptyPaneOrWindow() { 1723 if (this.getCenter().getActivePaneItem() != null) { 1724 this.getCenter() 1725 .getActivePane() 1726 .destroyActiveItem(); 1727 } else if (this.getCenter().getPanes().length > 1) { 1728 this.getCenter().destroyActivePane(); 1729 } else if (this.config.get('core.closeEmptyWindows')) { 1730 atom.close(); 1731 } 1732 } 1733 1734 // Increase the editor font size by 1px. 1735 increaseFontSize() { 1736 this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1); 1737 } 1738 1739 // Decrease the editor font size by 1px. 1740 decreaseFontSize() { 1741 const fontSize = this.config.get('editor.fontSize'); 1742 if (fontSize > 1) { 1743 this.config.set('editor.fontSize', fontSize - 1); 1744 } 1745 } 1746 1747 // Restore to the window's original editor font size. 1748 resetFontSize() { 1749 if (this.originalFontSize) { 1750 this.config.set('editor.fontSize', this.originalFontSize); 1751 } 1752 } 1753 1754 // Removes the item's uri from the list of potential items to reopen. 1755 itemOpened(item) { 1756 let uri; 1757 if (typeof item.getURI === 'function') { 1758 uri = item.getURI(); 1759 } else if (typeof item.getUri === 'function') { 1760 uri = item.getUri(); 1761 } 1762 1763 if (uri != null) { 1764 _.remove(this.destroyedItemURIs, uri); 1765 } 1766 } 1767 1768 // Adds the destroyed item's uri to the list of items to reopen. 1769 didDestroyPaneItem({ item }) { 1770 let uri; 1771 if (typeof item.getURI === 'function') { 1772 uri = item.getURI(); 1773 } else if (typeof item.getUri === 'function') { 1774 uri = item.getUri(); 1775 } 1776 1777 if (uri != null) { 1778 this.destroyedItemURIs.push(uri); 1779 } 1780 } 1781 1782 // Called by Model superclass when destroyed 1783 destroyed() { 1784 this.paneContainers.center.destroy(); 1785 this.paneContainers.left.destroy(); 1786 this.paneContainers.right.destroy(); 1787 this.paneContainers.bottom.destroy(); 1788 this.cancelStoppedChangingActivePaneItemTimeout(); 1789 if (this.activeItemSubscriptions != null) { 1790 this.activeItemSubscriptions.dispose(); 1791 } 1792 if (this.element) this.element.destroy(); 1793 } 1794 1795 /* 1796 Section: Pane Locations 1797 */ 1798 1799 // Essential: Get the {WorkspaceCenter} at the center of the editor window. 1800 getCenter() { 1801 return this.paneContainers.center; 1802 } 1803 1804 // Essential: Get the {Dock} to the left of the editor window. 1805 getLeftDock() { 1806 return this.paneContainers.left; 1807 } 1808 1809 // Essential: Get the {Dock} to the right of the editor window. 1810 getRightDock() { 1811 return this.paneContainers.right; 1812 } 1813 1814 // Essential: Get the {Dock} below the editor window. 1815 getBottomDock() { 1816 return this.paneContainers.bottom; 1817 } 1818 1819 getPaneContainers() { 1820 return [ 1821 this.paneContainers.center, 1822 this.paneContainers.left, 1823 this.paneContainers.right, 1824 this.paneContainers.bottom 1825 ]; 1826 } 1827 1828 getVisiblePaneContainers() { 1829 const center = this.getCenter(); 1830 return atom.workspace 1831 .getPaneContainers() 1832 .filter(container => container === center || container.isVisible()); 1833 } 1834 1835 /* 1836 Section: Panels 1837 1838 Panels are used to display UI related to an editor window. They are placed at one of the four 1839 edges of the window: left, right, top or bottom. If there are multiple panels on the same window 1840 edge they are stacked in order of priority: higher priority is closer to the center, lower 1841 priority towards the edge. 1842 1843 *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher 1844 priority, allowing fixed size panels to be closer to the edge. This allows control targets to 1845 remain more static for easier targeting by users that employ mice or trackpads. (See 1846 [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) 1847 */ 1848 1849 // Essential: Get an {Array} of all the panel items at the bottom of the editor window. 1850 getBottomPanels() { 1851 return this.getPanels('bottom'); 1852 } 1853 1854 // Essential: Adds a panel item to the bottom of the editor window. 1855 // 1856 // * `options` {Object} 1857 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1858 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1859 // latter. See {ViewRegistry::addViewProvider} for more information. 1860 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1861 // (default: true) 1862 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1863 // forced closer to the edges of the window. (default: 100) 1864 // 1865 // Returns a {Panel} 1866 addBottomPanel(options) { 1867 return this.addPanel('bottom', options); 1868 } 1869 1870 // Essential: Get an {Array} of all the panel items to the left of the editor window. 1871 getLeftPanels() { 1872 return this.getPanels('left'); 1873 } 1874 1875 // Essential: Adds a panel item to the left of the editor window. 1876 // 1877 // * `options` {Object} 1878 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1879 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1880 // latter. See {ViewRegistry::addViewProvider} for more information. 1881 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1882 // (default: true) 1883 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1884 // forced closer to the edges of the window. (default: 100) 1885 // 1886 // Returns a {Panel} 1887 addLeftPanel(options) { 1888 return this.addPanel('left', options); 1889 } 1890 1891 // Essential: Get an {Array} of all the panel items to the right of the editor window. 1892 getRightPanels() { 1893 return this.getPanels('right'); 1894 } 1895 1896 // Essential: Adds a panel item to the right of the editor window. 1897 // 1898 // * `options` {Object} 1899 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1900 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1901 // latter. See {ViewRegistry::addViewProvider} for more information. 1902 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1903 // (default: true) 1904 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1905 // forced closer to the edges of the window. (default: 100) 1906 // 1907 // Returns a {Panel} 1908 addRightPanel(options) { 1909 return this.addPanel('right', options); 1910 } 1911 1912 // Essential: Get an {Array} of all the panel items at the top of the editor window. 1913 getTopPanels() { 1914 return this.getPanels('top'); 1915 } 1916 1917 // Essential: Adds a panel item to the top of the editor window above the tabs. 1918 // 1919 // * `options` {Object} 1920 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1921 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1922 // latter. See {ViewRegistry::addViewProvider} for more information. 1923 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1924 // (default: true) 1925 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1926 // forced closer to the edges of the window. (default: 100) 1927 // 1928 // Returns a {Panel} 1929 addTopPanel(options) { 1930 return this.addPanel('top', options); 1931 } 1932 1933 // Essential: Get an {Array} of all the panel items in the header. 1934 getHeaderPanels() { 1935 return this.getPanels('header'); 1936 } 1937 1938 // Essential: Adds a panel item to the header. 1939 // 1940 // * `options` {Object} 1941 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1942 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1943 // latter. See {ViewRegistry::addViewProvider} for more information. 1944 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1945 // (default: true) 1946 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1947 // forced closer to the edges of the window. (default: 100) 1948 // 1949 // Returns a {Panel} 1950 addHeaderPanel(options) { 1951 return this.addPanel('header', options); 1952 } 1953 1954 // Essential: Get an {Array} of all the panel items in the footer. 1955 getFooterPanels() { 1956 return this.getPanels('footer'); 1957 } 1958 1959 // Essential: Adds a panel item to the footer. 1960 // 1961 // * `options` {Object} 1962 // * `item` Your panel content. It can be DOM element, a jQuery element, or 1963 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1964 // latter. See {ViewRegistry::addViewProvider} for more information. 1965 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1966 // (default: true) 1967 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1968 // forced closer to the edges of the window. (default: 100) 1969 // 1970 // Returns a {Panel} 1971 addFooterPanel(options) { 1972 return this.addPanel('footer', options); 1973 } 1974 1975 // Essential: Get an {Array} of all the modal panel items 1976 getModalPanels() { 1977 return this.getPanels('modal'); 1978 } 1979 1980 // Essential: Adds a panel item as a modal dialog. 1981 // 1982 // * `options` {Object} 1983 // * `item` Your panel content. It can be a DOM element, a jQuery element, or 1984 // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the 1985 // model option. See {ViewRegistry::addViewProvider} for more information. 1986 // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden 1987 // (default: true) 1988 // * `priority` (optional) {Number} Determines stacking order. Lower priority items are 1989 // forced closer to the edges of the window. (default: 100) 1990 // * `autoFocus` (optional) {Boolean|Element} true if you want modal focus managed for you by Atom. 1991 // Atom will automatically focus on this element or your modal panel's first tabbable element when the modal 1992 // opens and will restore the previously selected element when the modal closes. Atom will 1993 // also automatically restrict user tab focus within your modal while it is open. 1994 // (default: false) 1995 // 1996 // Returns a {Panel} 1997 addModalPanel(options = {}) { 1998 return this.addPanel('modal', options); 1999 } 2000 2001 // Essential: Returns the {Panel} associated with the given item. Returns 2002 // `null` when the item has no panel. 2003 // 2004 // * `item` Item the panel contains 2005 panelForItem(item) { 2006 for (let location in this.panelContainers) { 2007 const container = this.panelContainers[location]; 2008 const panel = container.panelForItem(item); 2009 if (panel != null) { 2010 return panel; 2011 } 2012 } 2013 return null; 2014 } 2015 2016 getPanels(location) { 2017 return this.panelContainers[location].getPanels(); 2018 } 2019 2020 addPanel(location, options) { 2021 if (options == null) { 2022 options = {}; 2023 } 2024 return this.panelContainers[location].addPanel( 2025 new Panel(options, this.viewRegistry) 2026 ); 2027 } 2028 2029 /* 2030 Section: Searching and Replacing 2031 */ 2032 2033 // Public: Performs a search across all files in the workspace. 2034 // 2035 // * `regex` {RegExp} to search with. 2036 // * `options` (optional) {Object} 2037 // * `paths` An {Array} of glob patterns to search within. 2038 // * `onPathsSearched` (optional) {Function} to be periodically called 2039 // with number of paths searched. 2040 // * `leadingContextLineCount` {Number} default `0`; The number of lines 2041 // before the matched line to include in the results object. 2042 // * `trailingContextLineCount` {Number} default `0`; The number of lines 2043 // after the matched line to include in the results object. 2044 // * `iterator` {Function} callback on each file found. 2045 // 2046 // Returns a {Promise} with a `cancel()` method that will cancel all 2047 // of the underlying searches that were started as part of this scan. 2048 scan(regex, options = {}, iterator) { 2049 if (_.isFunction(options)) { 2050 iterator = options; 2051 options = {}; 2052 } 2053 2054 // Find a searcher for every Directory in the project. Each searcher that is matched 2055 // will be associated with an Array of Directory objects in the Map. 2056 const directoriesForSearcher = new Map(); 2057 for (const directory of this.project.getDirectories()) { 2058 let searcher = options.ripgrep 2059 ? this.ripgrepDirectorySearcher 2060 : this.scandalDirectorySearcher; 2061 for (const directorySearcher of this.directorySearchers) { 2062 if (directorySearcher.canSearchDirectory(directory)) { 2063 searcher = directorySearcher; 2064 break; 2065 } 2066 } 2067 let directories = directoriesForSearcher.get(searcher); 2068 if (!directories) { 2069 directories = []; 2070 directoriesForSearcher.set(searcher, directories); 2071 } 2072 directories.push(directory); 2073 } 2074 2075 // Define the onPathsSearched callback. 2076 let onPathsSearched; 2077 if (_.isFunction(options.onPathsSearched)) { 2078 // Maintain a map of directories to the number of search results. When notified of a new count, 2079 // replace the entry in the map and update the total. 2080 const onPathsSearchedOption = options.onPathsSearched; 2081 let totalNumberOfPathsSearched = 0; 2082 const numberOfPathsSearchedForSearcher = new Map(); 2083 onPathsSearched = function(searcher, numberOfPathsSearched) { 2084 const oldValue = numberOfPathsSearchedForSearcher.get(searcher); 2085 if (oldValue) { 2086 totalNumberOfPathsSearched -= oldValue; 2087 } 2088 numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched); 2089 totalNumberOfPathsSearched += numberOfPathsSearched; 2090 return onPathsSearchedOption(totalNumberOfPathsSearched); 2091 }; 2092 } else { 2093 onPathsSearched = function() {}; 2094 } 2095 2096 // Kick off all of the searches and unify them into one Promise. 2097 const allSearches = []; 2098 directoriesForSearcher.forEach((directories, searcher) => { 2099 const searchOptions = { 2100 inclusions: options.paths || [], 2101 includeHidden: true, 2102 excludeVcsIgnores: this.config.get('core.excludeVcsIgnoredPaths'), 2103 exclusions: this.config.get('core.ignoredNames'), 2104 follow: this.config.get('core.followSymlinks'), 2105 leadingContextLineCount: options.leadingContextLineCount || 0, 2106 trailingContextLineCount: options.trailingContextLineCount || 0, 2107 PCRE2: options.PCRE2, 2108 didMatch: result => { 2109 if (!this.project.isPathModified(result.filePath)) { 2110 return iterator(result); 2111 } 2112 }, 2113 didError(error) { 2114 return iterator(null, error); 2115 }, 2116 didSearchPaths(count) { 2117 return onPathsSearched(searcher, count); 2118 } 2119 }; 2120 const directorySearcher = searcher.search( 2121 directories, 2122 regex, 2123 searchOptions 2124 ); 2125 allSearches.push(directorySearcher); 2126 }); 2127 const searchPromise = Promise.all(allSearches); 2128 2129 for (let buffer of this.project.getBuffers()) { 2130 if (buffer.isModified()) { 2131 const filePath = buffer.getPath(); 2132 if (!this.project.contains(filePath)) { 2133 continue; 2134 } 2135 var matches = []; 2136 buffer.scan(regex, match => matches.push(match)); 2137 if (matches.length > 0) { 2138 iterator({ filePath, matches }); 2139 } 2140 } 2141 } 2142 2143 // Make sure the Promise that is returned to the client is cancelable. To be consistent 2144 // with the existing behavior, instead of cancel() rejecting the promise, it should 2145 // resolve it with the special value 'cancelled'. At least the built-in find-and-replace 2146 // package relies on this behavior. 2147 let isCancelled = false; 2148 const cancellablePromise = new Promise((resolve, reject) => { 2149 const onSuccess = function() { 2150 if (isCancelled) { 2151 resolve('cancelled'); 2152 } else { 2153 resolve(null); 2154 } 2155 }; 2156 2157 const onFailure = function(error) { 2158 for (let promise of allSearches) { 2159 promise.cancel(); 2160 } 2161 reject(error); 2162 }; 2163 2164 searchPromise.then(onSuccess, onFailure); 2165 }); 2166 cancellablePromise.cancel = () => { 2167 isCancelled = true; 2168 // Note that cancelling all of the members of allSearches will cause all of the searches 2169 // to resolve, which causes searchPromise to resolve, which is ultimately what causes 2170 // cancellablePromise to resolve. 2171 allSearches.map(promise => promise.cancel()); 2172 }; 2173 2174 return cancellablePromise; 2175 } 2176 2177 // Public: Performs a replace across all the specified files in the project. 2178 // 2179 // * `regex` A {RegExp} to search with. 2180 // * `replacementText` {String} to replace all matches of regex with. 2181 // * `filePaths` An {Array} of file path strings to run the replace on. 2182 // * `iterator` A {Function} callback on each file with replacements: 2183 // * `options` {Object} with keys `filePath` and `replacements`. 2184 // 2185 // Returns a {Promise}. 2186 replace(regex, replacementText, filePaths, iterator) { 2187 return new Promise((resolve, reject) => { 2188 let buffer; 2189 const openPaths = this.project 2190 .getBuffers() 2191 .map(buffer => buffer.getPath()); 2192 const outOfProcessPaths = _.difference(filePaths, openPaths); 2193 2194 let inProcessFinished = !openPaths.length; 2195 let outOfProcessFinished = !outOfProcessPaths.length; 2196 const checkFinished = () => { 2197 if (outOfProcessFinished && inProcessFinished) { 2198 resolve(); 2199 } 2200 }; 2201 2202 if (!outOfProcessFinished.length) { 2203 let flags = 'g'; 2204 if (regex.multiline) { 2205 flags += 'm'; 2206 } 2207 if (regex.ignoreCase) { 2208 flags += 'i'; 2209 } 2210 2211 const task = Task.once( 2212 require.resolve('./replace-handler'), 2213 outOfProcessPaths, 2214 regex.source, 2215 flags, 2216 replacementText, 2217 () => { 2218 outOfProcessFinished = true; 2219 checkFinished(); 2220 } 2221 ); 2222 2223 task.on('replace:path-replaced', iterator); 2224 task.on('replace:file-error', error => { 2225 iterator(null, error); 2226 }); 2227 } 2228 2229 for (buffer of this.project.getBuffers()) { 2230 if (!filePaths.includes(buffer.getPath())) { 2231 continue; 2232 } 2233 const replacements = buffer.replace(regex, replacementText, iterator); 2234 if (replacements) { 2235 iterator({ filePath: buffer.getPath(), replacements }); 2236 } 2237 } 2238 2239 inProcessFinished = true; 2240 checkFinished(); 2241 }); 2242 } 2243 2244 checkoutHeadRevision(editor) { 2245 if (editor.getPath()) { 2246 const checkoutHead = async () => { 2247 const repository = await this.project.repositoryForDirectory( 2248 new Directory(editor.getDirectoryPath()) 2249 ); 2250 if (repository) repository.checkoutHeadForEditor(editor); 2251 }; 2252 2253 if (this.config.get('editor.confirmCheckoutHeadRevision')) { 2254 this.applicationDelegate.confirm( 2255 { 2256 message: 'Confirm Checkout HEAD Revision', 2257 detail: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`, 2258 buttons: ['OK', 'Cancel'] 2259 }, 2260 response => { 2261 if (response === 0) checkoutHead(); 2262 } 2263 ); 2264 } else { 2265 checkoutHead(); 2266 } 2267 } 2268 } 2269 };