when opened files has identical file names", ->
+ editor1 = null
+ editor2 = null
+ waitsForPromise ->
+ atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) ->
+ editor1 = o
+ atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) ->
+ editor2 = o
+ runs ->
+ expect(editor1.getUniqueTitle()).toBe 'sample-theme-1/.../main.js'
+ expect(editor2.getUniqueTitle()).toBe 'sample-theme-2/.../main.js'
+
+
it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", ->
observed = []
editor.onDidChangeTitle (title) -> observed.push(title)
@@ -272,6 +303,7 @@ describe "TextEditor", ->
describe "when soft-wrap is enabled and code is folded", ->
beforeEach ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.createFold(2, 3)
@@ -327,6 +359,7 @@ describe "TextEditor", ->
describe "when the cursor was moved down from the beginning of an indented soft-wrapped line", ->
it "moves to the beginning of the previous line", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.setCursorScreenPosition([3, 0])
@@ -379,6 +412,7 @@ describe "TextEditor", ->
describe "when the cursor is at the beginning of an indented soft-wrapped line", ->
it "moves to the beginning of the line's continuation on the next screen row", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.setCursorScreenPosition([3, 0])
@@ -446,6 +480,7 @@ describe "TextEditor", ->
describe "when line is wrapped and follow previous line indentation", ->
beforeEach ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
it "wraps to the end of the previous line", ->
@@ -604,6 +639,7 @@ describe "TextEditor", ->
describe "when soft wrap is on", ->
it "moves cursor to the beginning of the screen line", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([1, 2])
editor.moveToEndOfScreenLine()
@@ -623,6 +659,7 @@ describe "TextEditor", ->
describe ".moveToBeginningOfLine()", ->
it "moves cursor to the beginning of the buffer line", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([1, 2])
editor.moveToBeginningOfLine()
@@ -632,6 +669,7 @@ describe "TextEditor", ->
describe ".moveToEndOfLine()", ->
it "moves cursor to the end of the buffer line", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([0, 2])
editor.moveToEndOfLine()
@@ -642,6 +680,7 @@ describe "TextEditor", ->
describe "when soft wrap is on", ->
it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition [2, 5]
editor.addCursorAtScreenPosition [8, 7]
@@ -1331,7 +1370,7 @@ describe "TextEditor", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
waitsForPromise ->
- atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
+ atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
it 'selects the correct surrounding word for the given scoped setting', ->
coffeeEditor.setCursorBufferPosition [0, 9] # in the middle of quicksort
@@ -1523,6 +1562,7 @@ describe "TextEditor", ->
it "can add selections to soft-wrapped line segments", ->
editor.setSoftWrapped(true)
editor.setEditorWidthInChars(40)
+ editor.setDefaultCharWidth(1)
editor.setSelectedScreenRange([[3, 10], [3, 15]])
editor.addSelectionBelow()
@@ -1533,7 +1573,7 @@ describe "TextEditor", ->
it "takes atomic tokens into account", ->
waitsForPromise ->
- atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o
+ atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o
runs ->
editor.setSelectedBufferRange([[2, 1], [2, 3]])
@@ -1548,6 +1588,7 @@ describe "TextEditor", ->
describe "when lines are soft-wrapped", ->
beforeEach ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(40)
it "skips soft-wrap indentation tokens", ->
@@ -1633,6 +1674,7 @@ describe "TextEditor", ->
it "can add selections to soft-wrapped line segments", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(40)
editor.setSelectedScreenRange([[4, 10], [4, 15]])
@@ -1644,7 +1686,7 @@ describe "TextEditor", ->
it "takes atomic tokens into account", ->
waitsForPromise ->
- atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o
+ atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o
runs ->
editor.setSelectedBufferRange([[3, 1], [3, 2]])
@@ -1659,6 +1701,7 @@ describe "TextEditor", ->
describe "when lines are soft-wrapped", ->
beforeEach ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(40)
it "skips soft-wrap indentation tokens", ->
@@ -1769,9 +1812,11 @@ describe "TextEditor", ->
it "does not share selections between different edit sessions for the same buffer", ->
editor2 = null
waitsForPromise ->
- atom.project.open('sample.js').then (o) -> editor2 = o
+ atom.workspace.getActivePane().splitRight()
+ atom.workspace.open(editor.getPath()).then (o) -> editor2 = o
runs ->
+ expect(editor2.getText()).toBe(editor.getText())
editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]])
expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges()
@@ -3045,6 +3090,7 @@ describe "TextEditor", ->
describe "when soft wrap is on", ->
it "cuts up to the end of the line", ->
editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([2, 2])
editor.cutToEndOfLine()
@@ -4015,7 +4061,7 @@ describe "TextEditor", ->
editor.destroy()
waitsForPromise ->
- atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
+ atom.workspace.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe true
@@ -4088,7 +4134,7 @@ describe "TextEditor", ->
editor.destroy()
waitsForPromise ->
- atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
+ atom.workspace.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe true
@@ -4143,7 +4189,7 @@ describe "TextEditor", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
waitsForPromise ->
- atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
+ atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
afterEach: ->
atom.packages.deactivatePackages()
@@ -4346,7 +4392,7 @@ describe "TextEditor", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
waitsForPromise ->
- atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
+ atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
runs ->
atom.config.set('editor.autoIndent', true, scopeSelector: '.source.js')
@@ -4496,7 +4542,8 @@ describe "TextEditor", ->
editor2 = null
waitsForPromise ->
- atom.project.open('sample.js', autoIndent: false).then (o) -> editor2 = o
+ atom.workspace.getActivePane().splitRight()
+ atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o
runs ->
expect(editor.shouldPromptToSave()).toBeFalsy()
@@ -4750,11 +4797,10 @@ describe "TextEditor", ->
describe '.get/setPlaceholderText()', ->
it 'can be created with placeholderText', ->
- TextBuffer = require 'text-buffer'
- newEditor = new TextEditor
- buffer: new TextBuffer
+ newEditor = atom.workspace.buildTextEditor(
mini: true
placeholderText: 'yep'
+ )
expect(newEditor.getPlaceholderText()).toBe 'yep'
it 'models placeholderText and emits an event when changed', ->
@@ -4782,7 +4828,7 @@ describe "TextEditor", ->
describe "when there's no repository for the editor's file", ->
it "doesn't do anything", ->
- editor = new TextEditor({})
+ editor = atom.workspace.buildTextEditor()
editor.setText("stuff")
editor.checkoutHeadRevision()
diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee
index 6a03bb02f..dd528b37e 100644
--- a/spec/text-utils-spec.coffee
+++ b/spec/text-utils-spec.coffee
@@ -44,3 +44,33 @@ describe 'text utilities', ->
expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe false
expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe false
+
+ describe ".isDoubleWidthCharacter(character)", ->
+ it "returns true when the character is either japanese, chinese or a full width form", ->
+ expect(textUtils.isDoubleWidthCharacter("我")).toBe(true)
+
+ expect(textUtils.isDoubleWidthCharacter("私")).toBe(true)
+
+ expect(textUtils.isDoubleWidthCharacter("B")).toBe(true)
+ expect(textUtils.isDoubleWidthCharacter(",")).toBe(true)
+ expect(textUtils.isDoubleWidthCharacter("¢")).toBe(true)
+
+ expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
+
+ describe ".isHalfWidthCharacter(character)", ->
+ it "returns true when the character is an half width form", ->
+ expect(textUtils.isHalfWidthCharacter("ハ")).toBe(true)
+ expect(textUtils.isHalfWidthCharacter("ヒ")).toBe(true)
+ expect(textUtils.isHalfWidthCharacter("ᆲ")).toBe(true)
+ expect(textUtils.isHalfWidthCharacter("■")).toBe(true)
+
+ expect(textUtils.isHalfWidthCharacter("B")).toBe(false)
+
+ describe ".isKoreanCharacter(character)", ->
+ it "returns true when the character is a korean character", ->
+ expect(textUtils.isKoreanCharacter("우")).toBe(true)
+ expect(textUtils.isKoreanCharacter("가")).toBe(true)
+ expect(textUtils.isKoreanCharacter("ㅢ")).toBe(true)
+ expect(textUtils.isKoreanCharacter("ㄼ")).toBe(true)
+
+ expect(textUtils.isKoreanCharacter("O")).toBe(false)
diff --git a/spec/token-iterator-spec.coffee b/spec/token-iterator-spec.coffee
index ee8ac25e5..f876d30d1 100644
--- a/spec/token-iterator-spec.coffee
+++ b/spec/token-iterator-spec.coffee
@@ -24,7 +24,9 @@ describe "TokenIterator", ->
end x
x
""")
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
tokenizedBuffer.setGrammar(grammar)
tokenIterator = tokenizedBuffer.tokenizedLineForRow(1).getTokenIterator()
diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee
index 4945b37ec..e87125195 100644
--- a/spec/tokenized-buffer-spec.coffee
+++ b/spec/tokenized-buffer-spec.coffee
@@ -27,7 +27,9 @@ describe "TokenizedBuffer", ->
describe "when the buffer is destroyed", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
startTokenizing(tokenizedBuffer)
it "stops tokenization", ->
@@ -39,7 +41,9 @@ describe "TokenizedBuffer", ->
describe "when the buffer contains soft-tabs", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
startTokenizing(tokenizedBuffer)
tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler')
@@ -345,7 +349,9 @@ describe "TokenizedBuffer", ->
runs ->
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
startTokenizing(tokenizedBuffer)
afterEach ->
@@ -450,7 +456,9 @@ describe "TokenizedBuffer", ->
'abc\uD835\uDF97def'
//\uD835\uDF97xyz
"""
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
afterEach ->
@@ -487,7 +495,7 @@ describe "TokenizedBuffer", ->
tokenizedHandler = jasmine.createSpy("tokenized handler")
waitsForPromise ->
- atom.project.open('sample.js').then (o) -> editor = o
+ atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
@@ -500,7 +508,7 @@ describe "TokenizedBuffer", ->
tokenizedHandler = jasmine.createSpy("tokenized handler")
waitsForPromise ->
- atom.project.open('sample.js').then (o) -> editor = o
+ atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
@@ -518,7 +526,7 @@ describe "TokenizedBuffer", ->
tokenizedHandler = jasmine.createSpy("tokenized handler")
waitsForPromise ->
- atom.project.open('coffee.coffee').then (o) -> editor = o
+ atom.workspace.open('coffee.coffee').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
@@ -544,7 +552,9 @@ describe "TokenizedBuffer", ->
runs ->
buffer = atom.project.bufferForPathSync()
buffer.setText "<%= User.find(2).full_name %>
"
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('test.erb'))
fullyTokenize(tokenizedBuffer)
@@ -566,7 +576,9 @@ describe "TokenizedBuffer", ->
it "returns the correct token (regression)", ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"]
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"]
@@ -575,7 +587,10 @@ describe "TokenizedBuffer", ->
describe ".bufferRangeForScopeAtPosition(selector, position)", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars,
+ packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
describe "when the selector does not match the token at the position", ->
@@ -595,7 +610,9 @@ describe "TokenizedBuffer", ->
it "updates the tab length of the tokenized lines", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 6)
@@ -604,7 +621,9 @@ describe "TokenizedBuffer", ->
it "does not allow the tab length to be less than 1", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 1)
@@ -617,7 +636,9 @@ describe "TokenizedBuffer", ->
it "updates the tokens with the appropriate invisible characters", ->
buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ")
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
atom.config.set("editor.showInvisibles", true)
@@ -630,7 +651,9 @@ describe "TokenizedBuffer", ->
it "assigns endOfLineInvisibles to tokenized lines", ->
buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending")
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
atom.config.set('editor.showInvisibles', true)
atom.config.set("editor.invisibles", cr: 'R', eol: 'N')
@@ -651,7 +674,9 @@ describe "TokenizedBuffer", ->
describe "leading and trailing whitespace", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", ->
@@ -709,7 +734,9 @@ describe "TokenizedBuffer", ->
describe ".indentLevel on tokenized lines", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
describe "when the line is non-empty", ->
@@ -804,7 +831,9 @@ describe "TokenizedBuffer", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.insert [10, 0], " // multi-line\n // comment\n // block\n"
buffer.insert [0, 0], "// multi-line\n// comment\n// block\n"
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidChange (change) ->
delete change.bufferChange
@@ -881,7 +910,9 @@ describe "TokenizedBuffer", ->
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
buffer.setText('a\nb\nc')
- tokenizedBuffer = new TokenizedBuffer({buffer})
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
tokenizeCallback = jasmine.createSpy('onDidTokenize')
tokenizedBuffer.onDidTokenize(tokenizeCallback)
@@ -905,7 +936,7 @@ describe "TokenizedBuffer", ->
registration = atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', -> called = true)
waitsForPromise ->
- atom.project.open('sample.js', autoIndent: false).then (o) ->
+ atom.workspace.open('sample.js', autoIndent: false).then (o) ->
editor = o
waitsForPromise ->
diff --git a/spec/tokenized-line-spec.coffee b/spec/tokenized-line-spec.coffee
index 2914ec089..f1dce7b9e 100644
--- a/spec/tokenized-line-spec.coffee
+++ b/spec/tokenized-line-spec.coffee
@@ -7,7 +7,7 @@ describe "TokenizedLine", ->
describe "::isOnlyWhitespace()", ->
beforeEach ->
waitsForPromise ->
- atom.project.open('coffee.coffee').then (o) -> editor = o
+ atom.workspace.open('coffee.coffee').then (o) -> editor = o
it "returns true when the line is only whitespace", ->
expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true
diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee
index ec30ef2e3..bf0462ebf 100644
--- a/spec/tooltip-manager-spec.coffee
+++ b/spec/tooltip-manager-spec.coffee
@@ -8,7 +8,7 @@ describe "TooltipManager", ->
ctrlY = _.humanizeKeystroke("ctrl-y")
beforeEach ->
- manager = new TooltipManager
+ manager = new TooltipManager(keymapManager: atom.keymaps)
element = document.createElement('div')
element.classList.add('foo')
jasmine.attachToDOM(element)
diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee
index 345c0fd8c..3148942b4 100644
--- a/spec/window-event-handler-spec.coffee
+++ b/spec/window-event-handler-spec.coffee
@@ -4,11 +4,13 @@ fs = require 'fs-plus'
temp = require 'temp'
TextEditor = require '../src/text-editor'
WindowEventHandler = require '../src/window-event-handler'
+ipc = require 'ipc'
-describe "Window", ->
+describe "WindowEventHandler", ->
[projectPath, windowEventHandler] = []
beforeEach ->
+ atom.uninstallWindowEventHandler()
spyOn(atom, 'hide')
initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake ->
@@ -16,12 +18,12 @@ describe "Window", ->
loadSettings.initialPath = initialPath
loadSettings
atom.project.destroy()
- atom.windowEventHandler.unsubscribe()
- windowEventHandler = new WindowEventHandler
+ windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate, window, document})
projectPath = atom.project.getPaths()[0]
afterEach ->
windowEventHandler.unsubscribe()
+ atom.installWindowEventHandler()
describe "when the window is loaded", ->
it "doesn't have .is-blurred on the body tag", ->
@@ -51,64 +53,25 @@ describe "Window", ->
describe "beforeunload event", ->
beforeEach ->
jasmine.unspy(TextEditor.prototype, "shouldPromptToSave")
+ spyOn(ipc, 'send')
describe "when pane items are modified", ->
- it "prompts user to save and calls atom.workspace.confirmClose", ->
- editor = null
- spyOn(atom.workspace, 'confirmClose').andCallThrough()
- spyOn(atom, "confirm").andReturn(2)
+ editor = null
+ beforeEach ->
+ waitsForPromise -> atom.workspace.open("sample.js").then (o) -> editor = o
+ runs -> editor.insertText("I look different, I feel different.")
- waitsForPromise ->
- atom.workspace.open("sample.js").then (o) -> editor = o
+ it "prompts the user to save them, and allows the unload to continue if they confirm", ->
+ spyOn(atom.workspace, 'confirmClose').andReturn(true)
+ window.dispatchEvent(new CustomEvent('beforeunload'))
+ expect(atom.workspace.confirmClose).toHaveBeenCalled()
+ expect(ipc.send).not.toHaveBeenCalledWith('did-cancel-window-unload')
- runs ->
- editor.insertText("I look different, I feel different.")
- window.dispatchEvent(new CustomEvent('beforeunload'))
- expect(atom.workspace.confirmClose).toHaveBeenCalled()
- expect(atom.confirm).toHaveBeenCalled()
-
- it "prompts user to save and handler returns true if don't save", ->
- editor = null
- spyOn(atom, "confirm").andReturn(2)
-
- waitsForPromise ->
- atom.workspace.open("sample.js").then (o) -> editor = o
-
- runs ->
- editor.insertText("I look different, I feel different.")
- window.dispatchEvent(new CustomEvent('beforeunload'))
- expect(atom.confirm).toHaveBeenCalled()
-
- it "prompts user to save and handler returns false if dialog is canceled", ->
- editor = null
- spyOn(atom, "confirm").andReturn(1)
- waitsForPromise ->
- atom.workspace.open("sample.js").then (o) -> editor = o
-
- runs ->
- editor.insertText("I look different, I feel different.")
- window.dispatchEvent(new CustomEvent('beforeunload'))
- expect(atom.confirm).toHaveBeenCalled()
-
- describe "when the same path is modified in multiple panes", ->
- it "prompts to save the item", ->
- return
- editor = null
- filePath = path.join(temp.mkdirSync('atom-file'), 'file.txt')
- fs.writeFileSync(filePath, 'hello')
- spyOn(atom.workspace, 'confirmClose').andCallThrough()
- spyOn(atom, 'confirm').andReturn(0)
-
- waitsForPromise ->
- atom.workspace.open(filePath).then (o) -> editor = o
-
- runs ->
- atom.workspace.getActivePane().splitRight(copyActiveItem: true)
- editor.setText('world')
- window.dispatchEvent(new CustomEvent('beforeunload'))
- expect(atom.workspace.confirmClose).toHaveBeenCalled()
- expect(atom.confirm.callCount).toBe 1
- expect(fs.readFileSync(filePath, 'utf8')).toBe 'world'
+ it "cancels the unload if the user selects cancel", ->
+ spyOn(atom.workspace, 'confirmClose').andReturn(false)
+ window.dispatchEvent(new CustomEvent('beforeunload'))
+ expect(atom.workspace.confirmClose).toHaveBeenCalled()
+ expect(ipc.send).toHaveBeenCalledWith('did-cancel-window-unload')
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
@@ -225,62 +188,6 @@ describe "Window", ->
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
- describe "the window:open-locations event", ->
- beforeEach ->
- spyOn(atom.workspace, 'open')
- atom.project.setPaths([])
-
- describe "when the opened path exists", ->
- it "adds it to the project's paths", ->
- pathToOpen = __filename
- atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}]
-
- waitsFor ->
- atom.project.getPaths().length is 1
-
- runs ->
- expect(atom.project.getPaths()[0]).toBe __dirname
-
- describe "when the opened path does not exist but its parent directory does", ->
- it "adds the parent directory to the project paths", ->
- pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
- atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}]
-
- waitsFor ->
- atom.project.getPaths().length is 1
-
- runs ->
- expect(atom.project.getPaths()[0]).toBe __dirname
-
- describe "when the opened path is a file", ->
- it "opens it in the workspace", ->
- pathToOpen = __filename
- atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}]
-
- waitsFor ->
- atom.workspace.open.callCount is 1
-
- runs ->
- expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename
-
-
- describe "when the opened path is a directory", ->
- it "does not open it in the workspace", ->
- pathToOpen = __dirname
- atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}]
- expect(atom.workspace.open.callCount).toBe 0
-
- describe "when the opened path is a uri", ->
- it "adds it to the project's paths as is", ->
- pathToOpen = 'remote://server:7644/some/dir/path'
- atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}]
-
- waitsFor ->
- atom.project.getPaths().length is 1
-
- runs ->
- expect(atom.project.getPaths()[0]).toBe pathToOpen
-
describe "when keydown events occur on the document", ->
it "dispatches the event via the KeymapManager and CommandRegistry", ->
dispatchedCommands = []
diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee
index c60508640..5d7343540 100644
--- a/spec/workspace-spec.coffee
+++ b/spec/workspace-spec.coffee
@@ -1,6 +1,7 @@
path = require 'path'
temp = require 'temp'
Workspace = require '../src/workspace'
+Project = require '../src/project'
Pane = require '../src/pane'
platform = require './spec-helper-platform'
_ = require 'underscore-plus'
@@ -8,11 +9,14 @@ fstream = require 'fstream'
fs = require 'fs-plus'
describe "Workspace", ->
- workspace = null
+ [workspace, setDocumentEdited] = []
beforeEach ->
+ workspace = atom.workspace
+ workspace.resetFontSize()
+ spyOn(atom.applicationDelegate, "confirm")
+ setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited')
atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')])
- atom.workspace = workspace = new Workspace
waits(1)
describe "serialization", ->
@@ -21,8 +25,16 @@ describe "Workspace", ->
projectState = atom.project.serialize()
atom.workspace.destroy()
atom.project.destroy()
- atom.project = atom.deserializers.deserialize(projectState)
- atom.workspace = Workspace.deserialize(workspaceState)
+ atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
+ atom.project.deserialize(projectState, atom.deserializers)
+ atom.workspace = new Workspace({
+ config: atom.config, project: atom.project, packageManager: atom.packages,
+ grammarRegistry: atom.grammars, deserializerManager: atom.deserializers,
+ notificationManager: atom.notifications, clipboard: atom.clipboard,
+ applicationDelegate: atom.applicationDelegate,
+ viewRegistry: atom.views, assert: atom.assert.bind(atom),
+ })
+ atom.workspace.deserialize(workspaceState, atom.deserializers)
describe "when the workspace contains text editors", ->
it "constructs the view with the same panes", ->
@@ -281,6 +293,90 @@ describe "Workspace", ->
expect(workspace.paneContainer.root.children[0]).toBe pane1
expect(workspace.paneContainer.root.children[1]).toBe pane4
+ describe "when the 'split' option is 'up'", ->
+ it "opens the editor in the topmost pane of the current pane axis", ->
+ pane1 = workspace.getActivePane()
+ pane2 = pane1.splitDown()
+ expect(workspace.getActivePane()).toBe pane2
+
+ editor = null
+ waitsForPromise ->
+ workspace.open('a', split: 'up').then (o) -> editor = o
+
+ runs ->
+ expect(workspace.getActivePane()).toBe pane1
+ expect(pane1.items).toEqual [editor]
+ expect(pane2.items).toEqual []
+
+ # Focus bottom pane and reopen the file on the top
+ waitsForPromise ->
+ pane2.focus()
+ workspace.open('a', split: 'up').then (o) -> editor = o
+
+ runs ->
+ expect(workspace.getActivePane()).toBe pane1
+ expect(pane1.items).toEqual [editor]
+ expect(pane2.items).toEqual []
+
+ describe "when a pane axis is the topmost sibling of the current pane", ->
+ it "opens the new item in the current pane", ->
+ editor = null
+ pane1 = workspace.getActivePane()
+ pane2 = pane1.splitUp()
+ pane3 = pane2.splitRight()
+ pane1.activate()
+ expect(workspace.getActivePane()).toBe pane1
+
+ waitsForPromise ->
+ workspace.open('a', split: 'up').then (o) -> editor = o
+
+ runs ->
+ expect(workspace.getActivePane()).toBe pane1
+ expect(pane1.items).toEqual [editor]
+
+ describe "when the 'split' option is 'down'", ->
+ it "opens the editor in the bottommost pane of the current pane axis", ->
+ editor = null
+ pane1 = workspace.getActivePane()
+ pane2 = null
+ waitsForPromise ->
+ workspace.open('a', split: 'down').then (o) -> editor = o
+
+ runs ->
+ pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0]
+ expect(workspace.getActivePane()).toBe pane2
+ expect(pane1.items).toEqual []
+ expect(pane2.items).toEqual [editor]
+
+ # Focus bottom pane and reopen the file on the right
+ waitsForPromise ->
+ pane1.focus()
+ workspace.open('a', split: 'down').then (o) -> editor = o
+
+ runs ->
+ expect(workspace.getActivePane()).toBe pane2
+ expect(pane1.items).toEqual []
+ expect(pane2.items).toEqual [editor]
+
+ describe "when a pane axis is the bottommost sibling of the current pane", ->
+ it "opens the new item in a new pane split to the bottom of the current pane", ->
+ editor = null
+ pane1 = workspace.getActivePane()
+ pane2 = pane1.splitDown()
+ pane1.activate()
+ expect(workspace.getActivePane()).toBe pane1
+ pane4 = null
+
+ waitsForPromise ->
+ workspace.open('a', split: 'down').then (o) -> editor = o
+
+ runs ->
+ pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0]
+ expect(workspace.getActivePane()).toBe pane4
+ expect(pane4.items).toEqual [editor]
+ expect(workspace.paneContainer.root.children[0]).toBe pane1
+ expect(workspace.paneContainer.root.children[1]).toBe pane2
+
describe "when an initialLine and initialColumn are specified", ->
it "moves the cursor to the indicated location", ->
waitsForPromise ->
@@ -333,7 +429,8 @@ describe "Workspace", ->
describe "when the file is over 20MB", ->
it "prompts the user to make sure they want to open a file this big", ->
spyOn(fs, 'getSizeSync').andReturn 20 * 1048577 # 20MB
- spyOn(atom, 'confirm').andCallFake -> selectedButtonIndex
+ atom.applicationDelegate.confirm.andCallFake -> selectedButtonIndex
+ atom.applicationDelegate.confirm()
selectedButtonIndex = 1 # cancel
editor = null
@@ -342,16 +439,16 @@ describe "Workspace", ->
runs ->
expect(editor).toBeUndefined()
- expect(atom.confirm).toHaveBeenCalled()
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
- atom.confirm.reset()
+ atom.applicationDelegate.confirm.reset()
selectedButtonIndex = 0 # open the file
waitsForPromise ->
workspace.open('sample.js').then (e) -> editor = e
runs ->
- expect(atom.confirm).toHaveBeenCalled()
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor.displayBuffer.largeFileMode).toBe true
describe "when passed a path that matches a custom opener", ->
@@ -614,7 +711,13 @@ describe "Workspace", ->
spyOn(jsPackage, 'loadGrammarsSync')
spyOn(coffeePackage, 'loadGrammarsSync')
- workspace2 = Workspace.deserialize(state)
+ workspace2 = new Workspace({
+ config: atom.config, project: atom.project, packageManager: atom.packages,
+ notificationManager: atom.notifications, deserializerManager: atom.deserializers,
+ clipboard: atom.clipboard, viewRegistry: atom.views, grammarRegistry: atom.grammars,
+ applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom)
+ })
+ workspace2.deserialize(state, atom.deserializers)
expect(jsPackage.loadGrammarsSync.callCount).toBe 1
expect(coffeePackage.loadGrammarsSync.callCount).toBe 1
@@ -666,7 +769,13 @@ describe "Workspace", ->
it "updates the title to contain the project's path", ->
document.title = null
- workspace2 = Workspace.deserialize(atom.workspace.serialize())
+ workspace2 = new Workspace({
+ config: atom.config, project: atom.project, packageManager: atom.packages,
+ notificationManager: atom.notifications, deserializerManager: atom.deserializers,
+ clipboard: atom.clipboard, viewRegistry: atom.views, grammarRegistry: atom.grammars,
+ applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom)
+ })
+ workspace2.deserialize(atom.workspace.serialize(), atom.deserializers)
item = atom.workspace.getActivePaneItem()
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]} - Atom"
workspace2.destroy()
@@ -679,15 +788,14 @@ describe "Workspace", ->
waitsForPromise -> atom.workspace.open('b')
runs ->
[item1, item2] = atom.workspace.getPaneItems()
- spyOn(atom, 'setDocumentEdited')
- it "calls atom.setDocumentEdited when the active item changes", ->
+ it "calls setDocumentEdited when the active item changes", ->
expect(atom.workspace.getActivePaneItem()).toBe item2
item1.insertText('a')
expect(item1.isModified()).toBe true
atom.workspace.getActivePane().activateNextItem()
- expect(atom.setDocumentEdited).toHaveBeenCalledWith(true)
+ expect(setDocumentEdited).toHaveBeenCalledWith(true)
it "calls atom.setDocumentEdited when the active item's modified status changes", ->
expect(atom.workspace.getActivePaneItem()).toBe item2
@@ -695,13 +803,13 @@ describe "Workspace", ->
advanceClock(item2.getBuffer().getStoppedChangingDelay())
expect(item2.isModified()).toBe true
- expect(atom.setDocumentEdited).toHaveBeenCalledWith(true)
+ expect(setDocumentEdited).toHaveBeenCalledWith(true)
item2.undo()
advanceClock(item2.getBuffer().getStoppedChangingDelay())
expect(item2.isModified()).toBe false
- expect(atom.setDocumentEdited).toHaveBeenCalledWith(false)
+ expect(setDocumentEdited).toHaveBeenCalledWith(false)
describe "adding panels", ->
class TestItem
@@ -956,7 +1064,7 @@ describe "Workspace", ->
results = []
waitsForPromise ->
- atom.project.open('a').then (o) ->
+ atom.workspace.open('a').then (o) ->
editor = o
editor.setText("Elephant")
@@ -974,7 +1082,7 @@ describe "Workspace", ->
results = []
waitsForPromise ->
- atom.project.open(temp.openSync().path).then (o) ->
+ atom.workspace.open(temp.openSync().path).then (o) ->
editor = o
editor.setText("Elephant")
@@ -1070,6 +1178,9 @@ describe "Workspace", ->
search: (directory, regex, options) -> fakeSearch = new FakeSearch(options)
})
+ waitsFor ->
+ atom.workspace.directorySearchers.length > 0
+
it "can override the DefaultDirectorySearcher on a per-directory basis", ->
foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'
numPathsSearchedInDir2 = 1
@@ -1193,7 +1304,7 @@ describe "Workspace", ->
results = []
waitsForPromise ->
- atom.project.open('sample.js').then (o) -> editor = o
+ atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
expect(editor.isModified()).toBeFalsy()
@@ -1214,7 +1325,7 @@ describe "Workspace", ->
results = []
waitsForPromise ->
- atom.project.open('sample-with-comments.js').then (o) -> editor = o
+ atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
waitsForPromise ->
atom.workspace.replace /items/gi, 'items', [commentFilePath], (result) ->
@@ -1229,7 +1340,7 @@ describe "Workspace", ->
results = []
waitsForPromise ->
- atom.project.open('sample.js').then (o) -> editor = o
+ atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg')
diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee
new file mode 100644
index 000000000..1999c6e83
--- /dev/null
+++ b/src/application-delegate.coffee
@@ -0,0 +1,169 @@
+_ = require 'underscore-plus'
+ipc = require 'ipc'
+remote = require 'remote'
+shell = require 'shell'
+webFrame = require 'web-frame'
+{Disposable} = require 'event-kit'
+{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
+
+module.exports =
+class ApplicationDelegate
+ open: (params) ->
+ ipc.send('open', params)
+
+ pickFolder: (callback) ->
+ responseChannel = "atom-pick-folder-response"
+ ipc.on responseChannel, (path) ->
+ ipc.removeAllListeners(responseChannel)
+ callback(path)
+ ipc.send("pick-folder", responseChannel)
+
+ getCurrentWindow: ->
+ remote.getCurrentWindow()
+
+ closeWindow: ->
+ ipc.send("call-window-method", "close")
+
+ getWindowSize: ->
+ [width, height] = remote.getCurrentWindow().getSize()
+ {width, height}
+
+ setWindowSize: (width, height) ->
+ remote.getCurrentWindow().setSize(width, height)
+
+ getWindowPosition: ->
+ [x, y] = remote.getCurrentWindow().getPosition()
+ {x, y}
+
+ setWindowPosition: (x, y) ->
+ ipc.send("call-window-method", "setPosition", x, y)
+
+ centerWindow: ->
+ ipc.send("call-window-method", "center")
+
+ focusWindow: ->
+ ipc.send("call-window-method", "focus")
+
+ showWindow: ->
+ ipc.send("call-window-method", "show")
+
+ hideWindow: ->
+ ipc.send("call-window-method", "hide")
+
+ restartWindow: ->
+ ipc.send("call-window-method", "restart")
+
+ isWindowMaximized: ->
+ remote.getCurrentWindow().isMaximized()
+
+ maximizeWindow: ->
+ ipc.send("call-window-method", "maximize")
+
+ isWindowFullScreen: ->
+ remote.getCurrentWindow().isFullScreen()
+
+ setWindowFullScreen: (fullScreen=false) ->
+ ipc.send("call-window-method", "setFullScreen", fullScreen)
+
+ openWindowDevTools: ->
+ remote.getCurrentWindow().openDevTools()
+
+ toggleWindowDevTools: ->
+ remote.getCurrentWindow().toggleDevTools()
+
+ executeJavaScriptInWindowDevTools: (code) ->
+ remote.getCurrentWindow().executeJavaScriptInDevTools(code)
+
+ setWindowDocumentEdited: (edited) ->
+ ipc.send("call-window-method", "setDocumentEdited", edited)
+
+ setRepresentedFilename: (filename) ->
+ ipc.send("call-window-method", "setRepresentedFilename", filename)
+
+ setRepresentedDirectoryPaths: (paths) ->
+ loadSettings = getWindowLoadSettings()
+ loadSettings['initialPaths'] = paths
+ setWindowLoadSettings(loadSettings)
+
+ setAutoHideWindowMenuBar: (autoHide) ->
+ ipc.send("call-window-method", "setAutoHideMenuBar", autoHide)
+
+ setWindowMenuBarVisibility: (visible) ->
+ remote.getCurrentWindow().setMenuBarVisibility(visible)
+
+ getPrimaryDisplayWorkAreaSize: ->
+ screen = remote.require 'screen'
+ screen.getPrimaryDisplay().workAreaSize
+
+ confirm: ({message, detailedMessage, buttons}) ->
+ buttons ?= {}
+ if _.isArray(buttons)
+ buttonLabels = buttons
+ else
+ buttonLabels = Object.keys(buttons)
+
+ dialog = remote.require('dialog')
+ chosen = dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'info'
+ message: message
+ detail: detailedMessage
+ buttons: buttonLabels
+ })
+
+ if _.isArray(buttons)
+ chosen
+ else
+ callback = buttons[buttonLabels[chosen]]
+ callback?()
+
+ showMessageDialog: (params) ->
+
+ showSaveDialog: (params) ->
+ if _.isString(params)
+ params = defaultPath: params
+ else
+ params = _.clone(params)
+ params.title ?= 'Save File'
+ params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
+ dialog = remote.require('dialog')
+ dialog.showSaveDialog remote.getCurrentWindow(), params
+
+ playBeepSound: ->
+ shell.beep()
+
+ onDidOpenLocations: (callback) ->
+ outerCallback = (message, detail) ->
+ if message is 'open-locations'
+ callback(detail)
+
+ ipc.on('message', outerCallback)
+ new Disposable ->
+ ipc.removeEventListener('message', outerCallback)
+
+ onUpdateAvailable: (callback) ->
+ outerCallback = (message, detail) ->
+ if message is 'update-available'
+ callback(detail)
+
+ ipc.on('message', outerCallback)
+ new Disposable ->
+ ipc.removeEventListener('message', outerCallback)
+
+ onApplicationMenuCommand: (callback) ->
+ ipc.on('command', callback)
+ new Disposable ->
+ ipc.removeEventListener('command', callback)
+
+ onContextMenuCommand: (callback) ->
+ ipc.on('context-command', callback)
+ new Disposable ->
+ ipc.removeEventListener('context-command', callback)
+
+ didCancelWindowUnload: ->
+ ipc.send('did-cancel-window-unload')
+
+ openExternal: (url) ->
+ shell.openExternal(url)
+
+ disablePinchToZoom: ->
+ webFrame.setZoomLevelLimits(1, 1)
diff --git a/src/atom.coffee b/src/atom-environment.coffee
similarity index 58%
rename from src/atom.coffee
rename to src/atom-environment.coffee
index 59d7765ac..004cfbc80 100644
--- a/src/atom.coffee
+++ b/src/atom-environment.coffee
@@ -1,9 +1,5 @@
crypto = require 'crypto'
-ipc = require 'ipc'
-os = require 'os'
path = require 'path'
-remote = require 'remote'
-shell = require 'shell'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
@@ -14,83 +10,52 @@ Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
+{getWindowLoadSettings} = require './window-load-settings-helpers'
+registerDefaultCommands = require './register-default-commands'
+
+DeserializerManager = require './deserializer-manager'
+ViewRegistry = require './view-registry'
+NotificationManager = require './notification-manager'
+Config = require './config'
+KeymapManager = require './keymap-extensions'
+TooltipManager = require './tooltip-manager'
+CommandRegistry = require './command-registry'
+GrammarRegistry = require './grammar-registry'
+StyleManager = require './style-manager'
+PackageManager = require './package-manager'
+ThemeManager = require './theme-manager'
+MenuManager = require './menu-manager'
+ContextMenuManager = require './context-menu-manager'
+CommandInstaller = require './command-installer'
+Clipboard = require './clipboard'
+Project = require './project'
+Workspace = require './workspace'
+PanelContainer = require './panel-container'
+Panel = require './panel'
+PaneContainer = require './pane-container'
+PaneAxis = require './pane-axis'
+Pane = require './pane'
+Project = require './project'
+TextEditor = require './text-editor'
+TextBuffer = require 'text-buffer'
+Gutter = require './gutter'
+
+WorkspaceElement = require './workspace-element'
+PanelContainerElement = require './panel-container-element'
+PanelElement = require './panel-element'
+PaneContainerElement = require './pane-container-element'
+PaneAxisElement = require './pane-axis-element'
+PaneElement = require './pane-element'
+TextEditorElement = require './text-editor-element'
+{createGutterView} = require './gutter-component-helpers'
# Essential: Atom global for dealing with packages, themes, menus, and the window.
#
# An instance of this class is always available as the `atom` global.
module.exports =
-class Atom extends Model
+class AtomEnvironment extends Model
@version: 1 # Increment this when the serialization format changes
- # Load or create the Atom environment in the given mode.
- #
- # * `mode` A {String} mode that is either 'editor' or 'spec' depending on the
- # kind of environment you want to build.
- #
- # Returns an Atom instance, fully initialized
- @loadOrCreate: (mode) ->
- startTime = Date.now()
- atom = @deserialize(@loadState(mode)) ? new this({mode, @version})
- atom.deserializeTimings.atom = Date.now() - startTime
- atom
-
- # Deserializes the Atom environment from a state object
- @deserialize: (state) ->
- new this(state) if state?.version is @version
-
- # Loads and returns the serialized state corresponding to this window
- # if it exists; otherwise returns undefined.
- @loadState: (mode) ->
- if stateKey = @getStateKey(@getLoadSettings().initialPaths, mode)
- if state = @getStorageFolder().load(stateKey)
- return state
-
- if windowState = @getLoadSettings().windowState
- try
- JSON.parse(@getLoadSettings().windowState)
- catch error
- console.warn "Error parsing window state: #{statePath} #{error.stack}", error
-
- # Returns the path where the state for the current window will be
- # located if it exists.
- @getStateKey: (paths, mode) ->
- if mode is 'spec'
- 'spec'
- else if mode is 'editor' and paths?.length > 0
- sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
- "editor-#{sha1}"
- else
- null
-
- # Get the directory path to Atom's configuration area.
- #
- # Returns the absolute path to ~/.atom
- @getConfigDirPath: ->
- @configDirPath ?= process.env.ATOM_HOME
-
- @getStorageFolder: ->
- @storageFolder ?= new StorageFolder(@getConfigDirPath())
-
- # Returns the load settings hash associated with the current window.
- @getLoadSettings: ->
- @loadSettings ?= JSON.parse(decodeURIComponent(location.hash.substr(1)))
- cloned = _.deepClone(@loadSettings)
- # The loadSettings.windowState could be large, request it only when needed.
- cloned.__defineGetter__ 'windowState', =>
- @getCurrentWindow().loadSettings.windowState
- cloned.__defineSetter__ 'windowState', (value) =>
- @getCurrentWindow().loadSettings.windowState = value
- cloned
-
- @updateLoadSetting: (key, value) ->
- @getLoadSettings()
- @loadSettings[key] = value
- location.hash = encodeURIComponent(JSON.stringify(@loadSettings))
-
- @getCurrentWindow: ->
- remote.getCurrentWindow()
-
- workspaceParentSelectorctor: 'body'
lastUncaughtError: null
###
@@ -150,122 +115,199 @@ class Atom extends Model
###
# Call .loadOrCreate instead
- constructor: (@state) ->
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- {@mode} = @state
- DeserializerManager = require './deserializer-manager'
- @deserializers = new DeserializerManager()
- @deserializeTimings = {}
+ constructor: (params={}) ->
+ {@applicationDelegate, @window, @document, configDirPath, @enablePersistence} = params
- # Sets up the basic services that should be available in all modes
- # (both spec and application).
- #
- # Call after this instance has been assigned to the `atom` global.
- initialize: ->
- window.onerror = =>
- @lastUncaughtError = Array::slice.call(arguments)
- [message, url, line, column, originalError] = @lastUncaughtError
-
- {line, column} = mapSourcePosition({source: url, line, column})
-
- eventObject = {message, url, line, column, originalError}
-
- openDevTools = true
- eventObject.preventDefault = -> openDevTools = false
-
- @emitter.emit 'will-throw-error', eventObject
-
- if openDevTools
- @openDevTools()
- @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
-
- @emitter.emit 'did-throw-error', {message, url, line, column, originalError}
-
- @disposables?.dispose()
- @disposables = new CompositeDisposable
-
- @displayWindow() unless @inSpecMode()
-
- @setBodyPlatformClass()
+ @state = {version: @constructor.version}
@loadTime = null
-
- Config = require './config'
- KeymapManager = require './keymap-extensions'
- ViewRegistry = require './view-registry'
- CommandRegistry = require './command-registry'
- TooltipManager = require './tooltip-manager'
- NotificationManager = require './notification-manager'
- PackageManager = require './package-manager'
- Clipboard = require './clipboard'
- GrammarRegistry = require './grammar-registry'
- ThemeManager = require './theme-manager'
- StyleManager = require './style-manager'
- ContextMenuManager = require './context-menu-manager'
- MenuManager = require './menu-manager'
{devMode, safeMode, resourcePath} = @getLoadSettings()
- configDirPath = @getConfigDirPath()
- # Add 'exports' to module search path.
- exportsPath = path.join(resourcePath, 'exports')
- require('module').globalPaths.push(exportsPath)
- # Still set NODE_PATH since tasks may need it.
- process.env.NODE_PATH = exportsPath
+ @emitter = new Emitter
+ @disposables = new CompositeDisposable
- # Make react.js faster
- process.env.NODE_ENV ?= 'production' unless devMode
+ @deserializers = new DeserializerManager(this)
+ @deserializeTimings = {}
+
+ @views = new ViewRegistry(this)
- @config = new Config({configDirPath, resourcePath})
- @keymaps = new KeymapManager({configDirPath, resourcePath})
- @keymaps.subscribeToFileReadFailure()
- @tooltips = new TooltipManager
@notifications = new NotificationManager
+
+ @config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
+ @setConfigSchema()
+
+ @keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications})
+
+ @tooltips = new TooltipManager(keymapManager: @keymaps)
+
@commands = new CommandRegistry
- @views = new ViewRegistry
- @registerViewProviders()
- @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode})
- @styles = new StyleManager
- document.head.appendChild(new StylesElement)
- @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode})
- @contextMenu = new ContextMenuManager({resourcePath, devMode})
- @menu = new MenuManager({resourcePath})
+ @commands.attach(@window)
+
+ @grammars = new GrammarRegistry({@config})
+
+ @styles = new StyleManager({configDirPath})
+
+ @packages = new PackageManager({
+ devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
+ commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
+ grammarRegistry: @grammars
+ })
+
+ @themes = new ThemeManager({
+ packageManager: @packages, configDirPath, resourcePath, safeMode, @config,
+ styleManager: @styles, notificationManager: @notifications, viewRegistry: @views
+ })
+
+ @menu = new MenuManager({resourcePath, keymapManager: @keymaps, packageManager: @packages})
+
+ @contextMenu = new ContextMenuManager({resourcePath, devMode, keymapManager: @keymaps})
+
+ @packages.setMenuManager(@menu)
+ @packages.setContextMenuManager(@contextMenu)
+ @packages.setThemeManager(@themes)
+
@clipboard = new Clipboard()
- @grammars = @deserializers.deserialize(@state.grammars ? @state.syntax) ? new GrammarRegistry()
- @disposables.add @packages.onDidActivateInitialPackages => @watchThemes()
- Project = require './project'
- TextBuffer = require 'text-buffer'
+ @project = new Project({notificationManager: @notifications, packageManager: @packages, @config})
+
+ @commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate)
+
+ @workspace = new Workspace({
+ @config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers,
+ notificationManager: @notifications, @applicationDelegate, @clipboard, viewRegistry: @views, assert: @assert.bind(this)
+ })
+ @themes.workspace = @workspace
+
+ @config.load()
+
+ @themes.loadBaseStylesheets()
+ @initialStyleElements = @styles.getSnapshot()
+ @themes.initialLoadComplete = true
+ @setBodyPlatformClass()
+
+ @stylesElement = @styles.buildStylesElement()
+ @document.head.appendChild(@stylesElement)
+
+ @applicationDelegate.disablePinchToZoom()
+
+ @keymaps.subscribeToFileReadFailure()
+ @keymaps.loadBundledKeymaps()
+
+ @registerDefaultCommands()
+ @registerDefaultOpeners()
+ @registerDefaultDeserializers()
+ @registerDefaultViewProviders()
+
+ @installUncaughtErrorHandler()
+ @installWindowEventHandler()
+
+ @observeAutoHideMenuBar()
+
+ setConfigSchema: ->
+ @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
+
+ registerDefaultDeserializers: ->
+ @deserializers.add(Workspace)
+ @deserializers.add(PaneContainer)
+ @deserializers.add(PaneAxis)
+ @deserializers.add(Pane)
+ @deserializers.add(Project)
+ @deserializers.add(TextEditor)
@deserializers.add(TextBuffer)
- TokenizedBuffer = require './tokenized-buffer'
- DisplayBuffer = require './display-buffer'
- TextEditor = require './text-editor'
- @windowEventHandler = new WindowEventHandler
+ registerDefaultCommands: ->
+ registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller})
- # Register the core views as early as possible in case they are needed for
- # package deserialization.
- registerViewProviders: ->
- Gutter = require './gutter'
- Pane = require './pane'
- PaneElement = require './pane-element'
- PaneContainer = require './pane-container'
- PaneContainerElement = require './pane-container-element'
- PaneAxis = require './pane-axis'
- PaneAxisElement = require './pane-axis-element'
- TextEditor = require './text-editor'
- TextEditorElement = require './text-editor-element'
- {createGutterView} = require './gutter-component-helpers'
+ registerDefaultViewProviders: ->
+ @views.addViewProvider Workspace, (model, env) ->
+ new WorkspaceElement().initialize(model, env)
+ @views.addViewProvider PanelContainer, (model, env) ->
+ new PanelContainerElement().initialize(model, env)
+ @views.addViewProvider Panel, (model, env) ->
+ new PanelElement().initialize(model, env)
+ @views.addViewProvider PaneContainer, (model, env) ->
+ new PaneContainerElement().initialize(model, env)
+ @views.addViewProvider PaneAxis, (model, env) ->
+ new PaneAxisElement().initialize(model, env)
+ @views.addViewProvider Pane, (model, env) ->
+ new PaneElement().initialize(model, env)
+ @views.addViewProvider TextEditor, (model, env) ->
+ new TextEditorElement().initialize(model, env)
+ @views.addViewProvider(Gutter, createGutterView)
- atom.views.addViewProvider PaneContainer, (model) ->
- new PaneContainerElement().initialize(model)
- atom.views.addViewProvider PaneAxis, (model) ->
- new PaneAxisElement().initialize(model)
- atom.views.addViewProvider Pane, (model) ->
- new PaneElement().initialize(model)
- atom.views.addViewProvider TextEditor, (model) ->
- new TextEditorElement().initialize(model)
- atom.views.addViewProvider(Gutter, createGutterView)
+ registerDefaultOpeners: ->
+ @workspace.addOpener (uri) =>
+ switch uri
+ when 'atom://.atom/stylesheet'
+ @workspace.open(@styles.getUserStyleSheetPath())
+ when 'atom://.atom/keymap'
+ @workspace.open(@keymaps.getUserKeymapPath())
+ when 'atom://.atom/config'
+ @workspace.open(@config.getUserConfigPath())
+ when 'atom://.atom/init-script'
+ @workspace.open(@getUserInitScriptPath())
+
+ registerDefaultTargetForKeymaps: ->
+ @keymaps.defaultTarget = @views.getView(@workspace)
+
+ observeAutoHideMenuBar: ->
+ @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
+ @setAutoHideMenuBar(newValue)
+ @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar')
+
+ reset: ->
+ @deserializers.clear()
+ @registerDefaultDeserializers()
+
+ @config.clear()
+ @setConfigSchema()
+
+ @keymaps.clear()
+ @keymaps.loadBundledKeymaps()
+
+ @commands.clear()
+ @registerDefaultCommands()
+
+ @styles.restoreSnapshot(@initialStyleElements)
+
+ @menu.clear()
+
+ @clipboard.reset()
+
+ @notifications.clear()
+
+ @contextMenu.clear()
+
+ @packages.reset()
+
+ @workspace.reset(@packages)
+ @registerDefaultOpeners()
+
+ @project.reset(@packages)
+
+ @workspace.subscribeToEvents()
+
+ @grammars.clear()
+
+ @views.clear()
+ @registerDefaultViewProviders()
+
+ @state.packageStates = {}
+ delete @state.workspace
+
+ destroy: ->
+ return if not @project
+
+ @disposables.dispose()
+ @workspace?.destroy()
+ @workspace = null
+ @themes.workspace = null
+ @project?.destroy()
+ @project = null
+ @commands.clear()
+ @stylesElement.remove()
+
+ @uninstallWindowEventHandler()
###
Section: Event Subscription
@@ -341,12 +383,6 @@ class Atom extends Model
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
- # Public: Get the directory path to Atom's configuration area.
- #
- # Returns the absolute path to `~/.atom`.
- getConfigDirPath: ->
- @constructor.getConfigDirPath()
-
# Public: Get the time taken to completely load the current window.
#
# This time include things like loading and activating packages, creating
@@ -361,7 +397,7 @@ class Atom extends Model
#
# Returns an {Object} containing all the load setting key/value pairs.
getLoadSettings: ->
- @constructor.getLoadSettings()
+ getWindowLoadSettings()
###
Section: Managing The Atom Window
@@ -372,7 +408,7 @@ class Atom extends Model
# Calling this method without an options parameter will open a prompt to pick
# a file/folder to open in the new window.
#
- # * `options` An {Object} with the following keys:
+ # * `params` An {Object} with the following keys:
# * `pathsToOpen` An {Array} of {String} paths to open.
# * `newWindow` A {Boolean}, true to always open a new window instead of
# reusing existing windows depending on the paths to open.
@@ -381,8 +417,8 @@ class Atom extends Model
# repository and also loads all the packages in ~/.atom/dev/packages
# * `safeMode` A {Boolean}, true to open the window in safe mode. Safe
# mode prevents all packages installed to ~/.atom/packages from loading.
- open: (options) ->
- ipc.send('open', options)
+ open: (params) ->
+ @applicationDelegate.open(params)
# Extended: Prompt the user to select one or more folders.
#
@@ -390,87 +426,81 @@ class Atom extends Model
# * `paths` An {Array} of {String} paths that the user selected, or `null`
# if the user dismissed the dialog.
pickFolder: (callback) ->
- responseChannel = "atom-pick-folder-response"
- ipc.on responseChannel, (path) ->
- ipc.removeAllListeners(responseChannel)
- callback(path)
- ipc.send("pick-folder", responseChannel)
+ @applicationDelegate.pickFolder(callback)
# Essential: Close the current window.
close: ->
- @getCurrentWindow().close()
+ @applicationDelegate.closeWindow()
# Essential: Get the size of current window.
#
# Returns an {Object} in the format `{width: 1000, height: 700}`
getSize: ->
- [width, height] = @getCurrentWindow().getSize()
- {width, height}
+ @applicationDelegate.getWindowSize()
# Essential: Set the size of current window.
#
# * `width` The {Number} of pixels.
# * `height` The {Number} of pixels.
setSize: (width, height) ->
- @getCurrentWindow().setSize(width, height)
+ @applicationDelegate.setWindowSize(width, height)
# Essential: Get the position of current window.
#
# Returns an {Object} in the format `{x: 10, y: 20}`
getPosition: ->
- [x, y] = @getCurrentWindow().getPosition()
- {x, y}
+ @applicationDelegate.getWindowPosition()
# Essential: Set the position of current window.
#
# * `x` The {Number} of pixels.
# * `y` The {Number} of pixels.
setPosition: (x, y) ->
- ipc.send('call-window-method', 'setPosition', x, y)
+ @applicationDelegate.setWindowPosition(x, y)
# Extended: Get the current window
getCurrentWindow: ->
- @constructor.getCurrentWindow()
+ @applicationDelegate.getCurrentWindow()
# Extended: Move current window to the center of the screen.
center: ->
- ipc.send('call-window-method', 'center')
+ @applicationDelegate.centerWindow()
# Extended: Focus the current window.
focus: ->
- ipc.send('call-window-method', 'focus')
- window.focus()
+ @applicationDelegate.focusWindow()
+ @window.focus()
# Extended: Show the current window.
show: ->
- ipc.send('call-window-method', 'show')
+ @applicationDelegate.showWindow()
# Extended: Hide the current window.
hide: ->
- ipc.send('call-window-method', 'hide')
+ @applicationDelegate.hideWindow()
# Extended: Reload the current window.
reload: ->
- ipc.send('call-window-method', 'restart')
+ @applicationDelegate.restartWindow()
# Extended: Returns a {Boolean} that is `true` if the current window is maximized.
isMaximized: ->
- @getCurrentWindow().isMaximized()
+ @applicationDelegate.isWindowMaximized()
maximize: ->
- ipc.send('call-window-method', 'maximize')
+ @applicationDelegate.maximizeWindow()
# Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode.
isFullScreen: ->
- @getCurrentWindow().isFullScreen()
+ @applicationDelegate.isWindowFullScreen()
# Extended: Set the full screen state of the current window.
setFullScreen: (fullScreen=false) ->
- ipc.send('call-window-method', 'setFullScreen', fullScreen)
+ @applicationDelegate.setWindowFullScreen(fullScreen)
if fullScreen
- document.body.classList.add("fullscreen")
+ @document.body.classList.add("fullscreen")
else
- document.body.classList.remove("fullscreen")
+ @document.body.classList.remove("fullscreen")
# Extended: Toggle the full screen state of the current window.
toggleFullScreen: ->
@@ -546,8 +576,7 @@ class Atom extends Model
if @isValidDimensions(dimensions)
dimensions
else
- screen = remote.require 'screen'
- {width, height} = screen.getPrimaryDisplay().workAreaSize
+ {width, height} = @applicationDelegate.getPrimaryDisplayWorkAreaSize()
{x: 0, y: 0, width: Math.min(1024, width), height}
restoreWindowDimensions: ->
@@ -565,37 +594,34 @@ class Atom extends Model
return if @inSpecMode()
workspaceElement = @views.getView(@workspace)
- backgroundColor = window.getComputedStyle(workspaceElement)['background-color']
- window.localStorage.setItem('atom:window-background-color', backgroundColor)
+ backgroundColor = @window.getComputedStyle(workspaceElement)['background-color']
+ @window.localStorage.setItem('atom:window-background-color', backgroundColor)
# Call this method when establishing a real application window.
startEditorWindow: ->
- {safeMode} = @getLoadSettings()
-
- CommandInstaller = require './command-installer'
-
- commandInstaller = new CommandInstaller(@getVersion())
- commandInstaller.installAtomCommand false, (error) ->
+ @commandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
- commandInstaller.installApmCommand false, (error) ->
+ @commandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
- @loadConfig()
- @keymaps.loadBundledKeymaps()
- @themes.loadBaseStylesheets()
+ @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
+ @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
+ @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
+ @listenForUpdates()
+
+ @registerDefaultTargetForKeymaps()
+
@packages.loadPackages()
- @deserializeEditorWindow()
+
+ @document.body.appendChild(@views.getView(@workspace))
@watchProjectPath()
@packages.activate()
@keymaps.loadUserKeymap()
- @requireUserInitScript() unless safeMode
+ @requireUserInitScript() unless @getLoadSettings().safeMode
@menu.update()
- @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
- @setAutoHideMenuBar(newValue)
- @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar')
@openInitialEmptyEditorIfNecessary()
@@ -603,36 +629,56 @@ class Atom extends Model
return if not @project
@storeWindowBackground()
- @state.grammars = @grammars.serialize()
+ @state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@packages.deactivatePackages()
@state.packageStates = @packages.packageStates
- @saveSync()
- @windowState = null
-
- removeEditorWindow: ->
- return if not @project
-
- @workspace?.destroy()
- @workspace = null
- @project?.destroy()
- @project = null
-
- @windowEventHandler?.unsubscribe()
+ @state.fullScreen = @isFullScreen()
+ @saveStateSync()
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0
@workspace.open(null)
+ installUncaughtErrorHandler: ->
+ @previousWindowErrorHandler = @window.onerror
+ @window.onerror = =>
+ @lastUncaughtError = Array::slice.call(arguments)
+ [message, url, line, column, originalError] = @lastUncaughtError
+
+ {line, column} = mapSourcePosition({source: url, line, column})
+
+ eventObject = {message, url, line, column, originalError}
+
+ openDevTools = true
+ eventObject.preventDefault = -> openDevTools = false
+
+ @emitter.emit 'will-throw-error', eventObject
+
+ if openDevTools
+ @openDevTools()
+ @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
+
+ @emitter.emit 'did-throw-error', {message, url, line, column, originalError}
+
+ uninstallUncaughtErrorHandler: ->
+ @window.onerror = @previousWindowErrorHandler
+
+ installWindowEventHandler: ->
+ @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate, @window, @document})
+
+ uninstallWindowEventHandler: ->
+ @windowEventHandler?.unsubscribe()
+
###
Section: Messaging the User
###
# Essential: Visually and audibly trigger a beep.
beep: ->
- shell.beep() if @config.get('core.audioBeep')
+ @applicationDelegate.playBeepSound() if @config.get('core.audioBeep')
@emitter.emit 'did-beep'
# Essential: A flexible way to open a dialog akin to an alert dialog.
@@ -655,25 +701,8 @@ class Atom extends Model
# button names and the values are callbacks to invoke when clicked.
#
# Returns the chosen button index {Number} if the buttons option was an array.
- confirm: ({message, detailedMessage, buttons}={}) ->
- buttons ?= {}
- if _.isArray(buttons)
- buttonLabels = buttons
- else
- buttonLabels = Object.keys(buttons)
-
- dialog = remote.require('dialog')
- chosen = dialog.showMessageBox @getCurrentWindow(),
- type: 'info'
- message: message
- detail: detailedMessage
- buttons: buttonLabels
-
- if _.isArray(buttons)
- chosen
- else
- callback = buttons[buttonLabels[chosen]]
- callback?()
+ confirm: (params={}) ->
+ @applicationDelegate.confirm(params)
###
Section: Managing the Dev Tools
@@ -681,15 +710,15 @@ class Atom extends Model
# Extended: Open the dev tools for the current window.
openDevTools: ->
- ipc.send('call-window-method', 'openDevTools')
+ @applicationDelegate.openWindowDevTools()
# Extended: Toggle the visibility of the dev tools for the current window.
toggleDevTools: ->
- ipc.send('call-window-method', 'toggleDevTools')
+ @applicationDelegate.toggleWindowDevTools()
# Extended: Execute code in dev tools.
executeJavaScriptInDevTools: (code) ->
- ipc.send('call-window-method', 'executeJavaScriptInDevTools', code)
+ @applicationDelegate.executeJavaScriptInWindowDevTools(code)
###
Section: Private
@@ -706,62 +735,19 @@ class Atom extends Model
false
- deserializeProject: ->
- Project = require './project'
-
- startTime = Date.now()
- @project ?= @deserializers.deserialize(@state.project) ? new Project()
- @deserializeTimings.project = Date.now() - startTime
-
- deserializeWorkspace: ->
- Workspace = require './workspace'
-
- startTime = Date.now()
- @workspace = Workspace.deserialize(@state.workspace) ? new Workspace
- @deserializeTimings.workspace = Date.now() - startTime
-
- workspaceElement = @views.getView(@workspace)
- @keymaps.defaultTarget = workspaceElement
- document.querySelector(@workspaceParentSelectorctor).appendChild(workspaceElement)
-
- deserializePackageStates: ->
- @packages.packageStates = @state.packageStates ? {}
- delete @state.packageStates
-
- deserializeEditorWindow: ->
- @deserializePackageStates()
- @deserializeProject()
- @deserializeWorkspace()
-
- loadConfig: ->
- @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
- @config.load()
-
loadThemes: ->
@themes.load()
- watchThemes: ->
- @themes.onDidChangeActiveThemes =>
- # Only reload stylesheets from non-theme packages
- for pack in @packages.getActivePackages() when pack.getType() isnt 'theme'
- pack.reloadStylesheets?()
- return
-
# Notify the browser project of the window's current project path
watchProjectPath: ->
@disposables.add @project.onDidChangePaths =>
- @constructor.updateLoadSetting('initialPaths', @project.getPaths())
-
- exit: (status) ->
- app = remote.require('app')
- app.emit('will-exit')
- remote.process.exit(status)
+ @applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
setDocumentEdited: (edited) ->
- ipc.send('call-window-method', 'setDocumentEdited', edited)
+ @applicationDelegate.setWindowDocumentEdited?(edited)
setRepresentedFilename: (filename) ->
- ipc.send('call-window-method', 'setRepresentedFilename', filename)
+ @applicationDelegate.setWindowRepresentedFilename?(filename)
addProjectFolder: ->
@pickFolder (selectedPaths = []) =>
@@ -771,27 +757,61 @@ class Atom extends Model
callback(showSaveDialogSync())
showSaveDialogSync: (options={}) ->
- if _.isString(options)
- options = defaultPath: options
- else
- options = _.clone(options)
- currentWindow = @getCurrentWindow()
- dialog = remote.require('dialog')
- options.title ?= 'Save File'
- options.defaultPath ?= @project?.getPaths()[0]
- dialog.showSaveDialog currentWindow, options
+ @applicationDelegate.showSaveDialog(options)
- saveSync: ->
- if storageKey = @constructor.getStateKey(@project?.getPaths(), @mode)
- @constructor.getStorageFolder().store(storageKey, @state)
+ saveStateSync: ->
+ return unless @enablePersistence
+
+ if storageKey = @getStateKey(@project?.getPaths())
+ @getStorageFolder().store(storageKey, @state)
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
- crashMainProcess: ->
- remote.process.crash()
+ loadStateSync: ->
+ return unless @enablePersistence
- crashRenderProcess: ->
- process.crash()
+ startTime = Date.now()
+
+ if stateKey = @getStateKey(@getLoadSettings().initialPaths)
+ if state = @getStorageFolder().load(stateKey)
+ @state = state
+
+ if not @state? and windowState = @getLoadSettings().windowState
+ try
+ if state = JSON.parse(@getLoadSettings().windowState)
+ @state = state
+ catch error
+ console.warn "Error parsing window state: #{statePath} #{error.stack}", error
+
+ @deserializeTimings.atom = Date.now() - startTime
+
+ if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath
+ @grammars.grammarOverridesByPath = grammarOverridesByPath
+
+ @setFullScreen(@state.fullScreen)
+
+ @packages.packageStates = @state.packageStates ? {}
+
+ startTime = Date.now()
+ @project.deserialize(@state.project, @deserializers) if @state.project?
+ @deserializeTimings.project = Date.now() - startTime
+
+ startTime = Date.now()
+ @workspace.deserialize(@state.workspace, @deserializers) if @state.workspace?
+ @deserializeTimings.workspace = Date.now() - startTime
+
+ getStateKey: (paths) ->
+ if paths?.length > 0
+ sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
+ "editor-#{sha1}"
+ else
+ null
+
+ getConfigDirPath: ->
+ @configDirPath ?= process.env.ATOM_HOME
+
+ getStorageFolder: ->
+ @storageFolder ?= new StorageFolder(@getConfigDirPath())
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
@@ -802,44 +822,52 @@ class Atom extends Model
try
require(userInitScriptPath) if fs.isFileSync(userInitScriptPath)
catch error
- atom.notifications.addError "Failed to load `#{userInitScriptPath}`",
+ @notifications.addError "Failed to load `#{userInitScriptPath}`",
detail: error.message
dismissable: true
- # Require the module with the given globals.
- #
- # The globals will be set on the `window` object and removed after the
- # require completes.
- #
- # * `id` The {String} module name or path.
- # * `globals` An optional {Object} to set as globals during require.
- requireWithGlobals: (id, globals={}) ->
- existingGlobals = {}
- for key, value of globals
- existingGlobals[key] = window[key]
- window[key] = value
-
- require(id)
-
- for key, value of existingGlobals
- if value is undefined
- delete window[key]
- else
- window[key] = value
- return
-
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
updateAvailable: (details) ->
@emitter.emit 'update-available', details
+ listenForUpdates: ->
+ @disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
+
setBodyPlatformClass: ->
- document.body.classList.add("platform-#{process.platform}")
+ @document.body.classList.add("platform-#{process.platform}")
setAutoHideMenuBar: (autoHide) ->
- ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide)
- ipc.send('call-window-method', 'setMenuBarVisibility', not autoHide)
+ @applicationDelegate.setAutoHideWindowMenuBar(autoHide)
+ @applicationDelegate.setWindowMenuBarVisibility(not autoHide)
+
+ dispatchApplicationMenuCommand: (command, arg) ->
+ activeElement = @document.activeElement
+ # Use the workspace element if body has focus
+ if activeElement is @document.body and workspaceElement = @views.getView(@workspace)
+ activeElement = workspaceElement
+ @commands.dispatch(activeElement, command, arg)
+
+ dispatchContextMenuCommand: (command, args...) ->
+ @commands.dispatch(@contextMenu.activeElement, command, args)
+
+ openLocations: (locations) ->
+ needsProjectPaths = @project?.getPaths().length is 0
+
+ for {pathToOpen, initialLine, initialColumn} in locations
+ if pathToOpen? and needsProjectPaths
+ if fs.existsSync(pathToOpen)
+ @project.addPath(pathToOpen)
+ else if fs.existsSync(path.dirname(pathToOpen))
+ @project.addPath(path.dirname(pathToOpen))
+ else
+ @project.addPath(pathToOpen)
+
+ unless fs.isDirectorySync(pathToOpen)
+ @workspace?.open(pathToOpen, {initialLine, initialColumn})
+
+ return
# Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner.
Promise.prototype.done = (callback) ->
diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee
index 0be160b5d..b68ee2c73 100644
--- a/src/browser/atom-application.coffee
+++ b/src/browser/atom-application.coffee
@@ -16,6 +16,8 @@ net = require 'net'
url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
+FindParentDir = null
+Resolve = null
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
@@ -63,7 +65,9 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
- {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath} = options
+ {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options
+
+ @socketPath = null if options.test
global.atomApplication = this
@@ -83,11 +87,11 @@ class AtomApplication
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test
@openWithOptions(options)
else
- @loadState() or @openPath(options)
+ @loadState(options) or @openPath(options)
- openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) ->
+ openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) ->
if test
- @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile})
+ @runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
else if urlsToOpen.length > 0
@@ -130,6 +134,7 @@ class AtomApplication
# the other launches will just pass their information to this server and then
# close immediately.
listenForArgumentsFromNewProcess: ->
+ return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
connection.on 'data', (data) =>
@@ -139,7 +144,7 @@ class AtomApplication
server.on 'error', (error) -> console.error 'Application server failed', error
deleteSocketFile: ->
- return if process.platform is 'win32'
+ return if process.platform is 'win32' or not @socketPath?
if fs.existsSync(@socketPath)
try
@@ -160,7 +165,6 @@ class AtomApplication
devMode: @focusedWindow()?.devMode
safeMode: @focusedWindow()?.safeMode
- @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: @devResourcePath, safeMode: @focusedWindow()?.safeMode)
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(_.extend(windowDimensions: @focusedWindow()?.getDimensions(), getLoadSettings()))
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@@ -215,11 +219,6 @@ class AtomApplication
@killAllProcesses()
@deleteSocketFile()
- app.on 'will-exit', =>
- @saveState(false)
- @killAllProcesses()
- @deleteSocketFile()
-
app.on 'open-file', (event, pathToOpen) =>
event.preventDefault()
@openPath({pathToOpen})
@@ -250,8 +249,8 @@ class AtomApplication
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu.update(win, template, keystrokesByCommand)
- ipc.on 'run-package-specs', (event, specDirectory) =>
- @runSpecs({resourcePath: @devResourcePath, specDirectory: specDirectory, exitWhenDone: false})
+ ipc.on 'run-package-specs', (event, packageSpecPath) =>
+ @runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
ipc.on 'command', (event, command) =>
@emit(command)
@@ -268,13 +267,19 @@ class AtomApplication
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
- ipc.on 'cancel-window-close', =>
+ ipc.on 'did-cancel-window-unload', =>
@quitting = false
clipboard = require '../safe-clipboard'
ipc.on 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
+ ipc.on 'write-to-stdout', (event, output) ->
+ process.stdout.write(output)
+
+ ipc.on 'write-to-stderr', (event, output) ->
+ process.stderr.write(output)
+
# Public: Executes the given command.
#
# If it isn't handled globally, delegate to the currently focused window.
@@ -373,14 +378,16 @@ class AtomApplication
unless pidToKillWhenClosed or newWindow
existingWindow = @windowForPaths(pathsToOpen, devMode)
-
- # Default to using the specified window or the last focused window
- currentWindow = window ? @lastFocusedWindow
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
- existingWindow ?= currentWindow if (
- stats.every((stat) -> stat.isFile?()) or
- stats.some((stat) -> stat.isDirectory?()) and not currentWindow?.hasProjectPath()
- )
+ unless existingWindow?
+ if currentWindow = window ? @lastFocusedWindow
+ existingWindow = currentWindow if (
+ currentWindow.devMode is devMode and
+ (
+ stats.every((stat) -> stat.isFile?()) or
+ stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath())
+ )
+ )
if existingWindow?
openedWindow = existingWindow
@@ -392,12 +399,12 @@ class AtomApplication
else
if devMode
try
- bootstrapScript = require.resolve(path.join(@devResourcePath, 'src', 'window-bootstrap'))
+ windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
- bootstrapScript ?= require.resolve('../window-bootstrap')
+ windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
- openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
+ openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -436,15 +443,15 @@ class AtomApplication
if states.length > 0 or allowEmpty
@storageFolder.store('application.json', states)
- loadState: ->
+ loadState: (options) ->
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
- @openWithOptions({
+ @openWithOptions(_.extend(options, {
pathsToOpen: state.initialPaths
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
- })
+ }))
true
else
false
@@ -472,9 +479,9 @@ class AtomApplication
if pack?
if pack.urlMain
packagePath = @packages.resolvePackagePath(packageName)
- bootstrapScript = path.resolve(packagePath, pack.urlMain)
+ windowInitializationScript = path.resolve(packagePath, pack.urlMain)
windowDimensions = @focusedWindow()?.getDimensions()
- new AtomWindow({bootstrapScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
+ new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
else
@@ -483,25 +490,63 @@ class AtomApplication
# Opens up a new {AtomWindow} to run specs within.
#
# options -
- # :exitWhenDone - A Boolean that, if true, will close the window upon
+ # :headless - A Boolean that, if true, will close the window upon
# completion.
# :resourcePath - The path to include specs from.
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
- runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode}) ->
+ runTests: ({headless, devMode, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
- try
- bootstrapScript = require.resolve(path.resolve(@devResourcePath, 'spec', 'spec-bootstrap'))
- catch error
- bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap'))
+ timeoutInSeconds = Number.parseFloat(timeout)
+ unless Number.isNaN(timeoutInSeconds)
+ timeoutHandler = ->
+ console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds."
+ process.exit(124) # Use the same exit code as the UNIX timeout util.
+ setTimeout(timeoutHandler, timeoutInSeconds * 1000)
+ try
+ windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window'))
+ catch error
+ windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window'))
+
+ testPaths = []
+ if pathsToOpen?
+ for pathToOpen in pathsToOpen
+ testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
+
+ if testPaths.length is 0
+ process.stderr.write 'Error: Specify at least one test path\n\n'
+ process.exit(1)
+
+ legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
+ testRunnerPath = @resolveTestRunnerPath(testPaths[0])
isSpec = true
- devMode = true
safeMode ?= false
- new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode})
+ new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode})
+
+ resolveTestRunnerPath: (testPath) ->
+ FindParentDir ?= require 'find-parent-dir'
+
+ if packageRoot = FindParentDir.sync(testPath, 'package.json')
+ packageMetadata = require(path.join(packageRoot, 'package.json'))
+ if packageMetadata.atomTestRunner
+ Resolve ?= require('resolve')
+ if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions))
+ return testRunnerPath
+ else
+ process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'"
+ process.exit(1)
+
+ @resolveLegacyTestRunnerPath()
+
+ resolveLegacyTestRunnerPath: ->
+ try
+ require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner'))
+ catch error
+ require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='') ->
return {pathToOpen} unless pathToOpen
diff --git a/src/browser/atom-protocol-handler.coffee b/src/browser/atom-protocol-handler.coffee
index 0865f3ad8..0fda8095b 100644
--- a/src/browser/atom-protocol-handler.coffee
+++ b/src/browser/atom-protocol-handler.coffee
@@ -29,7 +29,7 @@ class AtomProtocolHandler
# Creates the 'atom' custom protocol handler.
registerAtomProtocol: ->
- protocol.registerProtocol 'atom', (request) =>
+ protocol.registerFileProtocol 'atom', (request, callback) =>
relativePath = path.normalize(request.url.substr(7))
if relativePath.indexOf('assets/') is 0
@@ -41,4 +41,4 @@ class AtomProtocolHandler
filePath = path.join(loadPath, relativePath)
break if fs.statSyncNoException(filePath).isFile?()
- new protocol.RequestFileJob(filePath)
+ callback(filePath)
diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee
index 1372d46ec..16456d274 100644
--- a/src/browser/atom-window.coffee
+++ b/src/browser/atom-window.coffee
@@ -19,7 +19,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
- {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings
+ {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -29,6 +29,10 @@ class AtomWindow
'web-preferences':
'direct-write': true
'subpixel-font-scaling': true
+
+ if @isSpec
+ options['web-preferences']['page-visibility'] = true
+
# Don't set icon on Windows so the exe's ico will be used as window and
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if process.platform is 'linux'
@@ -84,7 +88,7 @@ class AtomWindow
hash: encodeURIComponent(JSON.stringify(loadSettings))
getLoadSettings: ->
- if @browserWindow.webContents?.loaded
+ if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
@@ -131,7 +135,7 @@ class AtomWindow
@browserWindow.destroy() if chosen is 0
@browserWindow.webContents.on 'crashed', =>
- global.atomApplication.exit(100) if @exitWhenDone
+ global.atomApplication.exit(100) if @headless
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
@@ -142,6 +146,10 @@ class AtomWindow
when 0 then @browserWindow.destroy()
when 1 then @browserWindow.restart()
+ @browserWindow.webContents.on 'will-navigate', (event, url) =>
+ unless url is @browserWindow.webContents.getUrl()
+ event.preventDefault()
+
@setupContextMenu()
if @isSpec
diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee
index ca711eb83..55ab2462b 100644
--- a/src/browser/auto-update-manager.coffee
+++ b/src/browser/auto-update-manager.coffee
@@ -17,14 +17,8 @@ class AutoUpdateManager
constructor: (@version, @testMode, @disabled) ->
@state = IdleState
- if process.platform is 'win32'
- # Squirrel for Windows can't handle query params
- # https://github.com/Squirrel/Squirrel.Windows/issues/132
- @feedUrl = 'https://atom.io/api/updates'
- else
- @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
- @feedUrl = "https://atom.io/api/updates?version=#{@version}"
-
+ @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
+ @feedUrl = "https://atom.io/api/updates?version=#{@version}"
process.nextTick => @setupAutoUpdater()
setupAutoUpdater: ->
diff --git a/src/browser/main.coffee b/src/browser/main.coffee
index f0709037d..990002671 100644
--- a/src/browser/main.coffee
+++ b/src/browser/main.coffee
@@ -99,9 +99,9 @@ parseCommandLine = ->
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
- options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which to run package specs (default: Atom\'s spec directory).')
options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
+ options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.string('socket-path')
@@ -121,7 +121,7 @@ parseCommandLine = ->
safeMode = args['safe']
pathsToOpen = args._
test = args['test']
- specDirectory = args['spec-directory']
+ timeout = args['timeout']
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
@@ -133,18 +133,9 @@ parseCommandLine = ->
if args['resource-path']
devMode = true
resourcePath = args['resource-path']
- else
- # Set resourcePath based on the specDirectory if running specs on atom core
- if specDirectory?
- packageDirectoryPath = path.join(specDirectory, '..')
- packageManifestPath = path.join(packageDirectoryPath, 'package.json')
- if fs.statSyncNoException(packageManifestPath)
- try
- packageManifest = JSON.parse(fs.readFileSync(packageManifestPath))
- resourcePath = packageDirectoryPath if packageManifest.name is 'atom'
- if devMode
- resourcePath ?= devResourcePath
+ devMode = true if test
+ resourcePath ?= devResourcePath if devMode
unless fs.statSyncNoException(resourcePath)
resourcePath = path.dirname(path.dirname(__dirname))
@@ -157,7 +148,7 @@ parseCommandLine = ->
devResourcePath = normalizeDriveLetterName(devResourcePath)
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
- version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory,
- logFile, socketPath, profileStartup}
+ version, pidToKillWhenClosed, devMode, safeMode, newWindow,
+ logFile, socketPath, profileStartup, timeout}
start()
diff --git a/src/clipboard.coffee b/src/clipboard.coffee
index 84ff9ab3b..9511bdda9 100644
--- a/src/clipboard.coffee
+++ b/src/clipboard.coffee
@@ -14,8 +14,12 @@ clipboard = require './safe-clipboard'
# ```
module.exports =
class Clipboard
- metadata: null
- signatureForMetadata: null
+ constructor: ->
+ @reset()
+
+ reset: ->
+ @metadata = null
+ @signatureForMetadata = null
# Creates an `md5` hash of some text.
#
diff --git a/src/coffee-script.js b/src/coffee-script.js
index 90f23bfa5..967d07cdd 100644
--- a/src/coffee-script.js
+++ b/src/coffee-script.js
@@ -29,6 +29,10 @@ exports.compile = function (sourceCode, filePath) {
Error.prepareStackTrace = previousPrepareStackTrace
}
+ if (process.platform === 'win32') {
+ filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/')
+ }
+
var output = CoffeeScript.compile(sourceCode, {
filename: filePath,
sourceFiles: [filePath],
diff --git a/src/command-installer.coffee b/src/command-installer.coffee
index 729aef84c..5a6a7f94e 100644
--- a/src/command-installer.coffee
+++ b/src/command-installer.coffee
@@ -26,7 +26,7 @@ symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) ->
module.exports =
class CommandInstaller
- constructor: (@appVersion) ->
+ constructor: (@appVersion, @applicationDelegate) ->
getInstallDirectory: ->
"/usr/local/bin"
@@ -36,7 +36,7 @@ class CommandInstaller
installShellCommandsInteractively: ->
showErrorDialog = (error) ->
- atom.confirm
+ @applicationDelegate.confirm
message: "Failed to install shell commands"
detailedMessage: error.message
@@ -44,11 +44,11 @@ class CommandInstaller
if error?
showErrorDialog(error)
else
- @installApmCommand true, (error) ->
+ @installApmCommand true, (error) =>
if error?
showErrorDialog(error)
else
- atom.confirm
+ @applicationDelegate.confirm
message: "Commands installed."
detailedMessage: "The shell commands `atom` and `apm` are installed."
diff --git a/src/command-registry.coffee b/src/command-registry.coffee
index 4cf002c7e..db2cf498d 100644
--- a/src/command-registry.coffee
+++ b/src/command-registry.coffee
@@ -44,15 +44,23 @@ SequenceCount = 0
# ```
module.exports =
class CommandRegistry
- constructor: (@rootNode) ->
+ constructor: ->
+ @rootNode = null
+ @clear()
+
+ clear: ->
@registeredCommands = {}
@selectorBasedListenersByCommandName = {}
@inlineListenersByCommandName = {}
@emitter = new Emitter
+ attach: (@rootNode) ->
+ @commandRegistered(command) for command of @selectorBasedListenersByCommandName
+ @commandRegistered(command) for command of @inlineListenersByCommandName
+
destroy: ->
for commandName of @registeredCommands
- window.removeEventListener(commandName, @handleCommandEvent, true)
+ @rootNode.removeEventListener(commandName, @handleCommandEvent, true)
return
# Public: Add one or more command listeners associated with a selector.
@@ -253,8 +261,8 @@ class CommandRegistry
matched
commandRegistered: (commandName) ->
- unless @registeredCommands[commandName]
- window.addEventListener(commandName, @handleCommandEvent, true)
+ if @rootNode? and not @registeredCommands[commandName]
+ @rootNode.addEventListener(commandName, @handleCommandEvent, true)
@registeredCommands[commandName] = true
class SelectorBasedListener
diff --git a/src/compile-cache.js b/src/compile-cache.js
index e10bf6803..f7726b4b3 100644
--- a/src/compile-cache.js
+++ b/src/compile-cache.js
@@ -158,6 +158,27 @@ require('source-map-support').install({
}
})
+var sourceMapPrepareStackTrace = Error.prepareStackTrace
+var prepareStackTrace = sourceMapPrepareStackTrace
+
+// Prevent coffee-script from reassigning Error.prepareStackTrace
+Object.defineProperty(Error, 'prepareStackTrace', {
+ get: function () { return prepareStackTrace },
+ set: function (newValue) {}
+})
+
+// Enable Grim to access the raw stack without reassigning Error.prepareStackTrace
+Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native
+ prepareStackTrace = getRawStack
+ var result = this.stack
+ prepareStackTrace = sourceMapPrepareStackTrace
+ return result
+}
+
+function getRawStack (_, stack) {
+ return stack
+}
+
Object.keys(COMPILERS).forEach(function (extension) {
var compiler = COMPILERS[extension]
diff --git a/src/config-schema.coffee b/src/config-schema.coffee
index d8eeb6fe3..08956d470 100644
--- a/src/config-schema.coffee
+++ b/src/config-schema.coffee
@@ -12,24 +12,27 @@ module.exports =
default: [".git", ".hg", ".svn", ".DS_Store", "._*", "Thumbs.db"]
items:
type: 'string'
+ description: 'List of string glob patterns. Files and directories matching these patterns will be ignored by some packages, such as the fuzzy finder and tree view. Individual packages might have additional config settings for ignoring names.'
excludeVcsIgnoredPaths:
type: 'boolean'
default: true
title: 'Exclude VCS Ignored Paths'
+ description: 'Files and directories ignored by the current project\'s VCS system will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.'
followSymlinks:
type: 'boolean'
default: true
title: 'Follow symlinks'
- description: 'Used when searching and when opening files with the fuzzy finder.'
+ description: 'Follow symbolic links when searching files and when opening files with the fuzzy finder.'
disabledPackages:
type: 'array'
default: []
items:
type: 'string'
+ description: 'List of names of installed packages which are not loaded at startup.'
customFileTypes:
type: 'object'
default: {}
- description: 'Associates scope names (e.g. "source.js") with arrays of file extensions and file names (e.g. ["Somefile", ".js2"])'
+ description: 'Associates scope names (e.g. `"source.js"`) with arrays of file extensions and file names (e.g. `["Somefile", ".js2"]`)'
additionalProperties:
type: 'array'
items:
@@ -39,15 +42,19 @@ module.exports =
default: ['one-dark-ui', 'one-dark-syntax']
items:
type: 'string'
+ description: 'Names of UI and syntax themes which will be used when Atom starts.'
projectHome:
type: 'string'
default: path.join(fs.getHomeDirectory(), 'github')
+ description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
audioBeep:
type: 'boolean'
default: true
+ description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.'
destroyEmptyPanes:
type: 'boolean'
default: true
+ description: 'When the last item of a pane is removed, remove that pane as well.'
fileEncoding:
description: 'Default character set encoding to use when reading and writing files.'
type: 'string'
@@ -90,7 +97,7 @@ module.exports =
'windows866'
]
openEmptyEditorOnStart:
- description: 'Automatically opens an empty editor when atom starts.'
+ description: 'Automatically open an empty editor on startup.'
type: 'boolean'
default: true
@@ -113,41 +120,51 @@ module.exports =
fontFamily:
type: 'string'
default: ''
+ description: 'The name of the font family used for editor text.'
fontSize:
type: 'integer'
default: 14
minimum: 1
maximum: 100
+ description: 'Height in pixels of editor text.'
lineHeight:
type: ['string', 'number']
default: 1.5
+ description: 'Height of editor lines, as a multiplier of font size.'
showInvisibles:
type: 'boolean'
default: false
+ description: 'Render placeholders for invisible characters, such as tabs, spaces and newlines.'
showIndentGuide:
type: 'boolean'
default: false
+ description: 'Show indentation indicators in the editor.'
showLineNumbers:
type: 'boolean'
default: true
+ description: 'Show line numbers in the editor\'s gutter.'
autoIndent:
type: 'boolean'
default: true
- description: 'Automatically indent the cursor when inserting a newline'
+ description: 'Automatically indent the cursor when inserting a newline.'
autoIndentOnPaste:
type: 'boolean'
default: true
+ description: 'Automatically indent pasted text based on the indentation of the previous line.'
nonWordCharacters:
type: 'string'
default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
+ description: 'A string of non-word characters to define word boundaries.'
preferredLineLength:
type: 'integer'
default: 80
minimum: 1
+ description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.'
tabLength:
type: 'integer'
default: 2
enum: [1, 2, 3, 4, 6, 8]
+ description: 'Number of spaces used to represent a tab.'
softWrap:
type: 'boolean'
default: false
@@ -155,32 +172,36 @@ module.exports =
softTabs:
type: 'boolean'
default: true
+ description: 'If the `Tab Type` config setting is set to "auto" and autodetection of tab type from buffer content fails, then this config setting determines whether a soft tab or a hard tab will be inserted when the Tab key is pressed.'
tabType:
type: 'string'
default: 'auto'
enum: ['auto', 'soft', 'hard']
- description: 'Determine character inserted during Tab keypress.'
+ description: 'Determine character inserted when Tab key is pressed. Possible values: "auto", "soft" and "hard". When set to "soft" or "hard", soft tabs (spaces) or hard tabs (tab characters) are used. When set to "auto", the editor auto-detects the tab type based on the contents of the buffer (it uses the first leading whitespace on a non-comment line), or uses the value of the Soft Tabs config setting if auto-detection fails.'
softWrapAtPreferredLineLength:
type: 'boolean'
default: false
- description: 'Will wrap to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when soft wrap is enabled globally or for the current language.'
+ description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language.'
softWrapHangingIndent:
type: 'integer'
default: 0
minimum: 0
+ description: 'When soft wrap is enabled, defines length of additional indentation applied to wrapped lines, in number of characters.'
scrollSensitivity:
type: 'integer'
default: 40
minimum: 10
maximum: 200
+ description: 'Determines how fast the editor scrolls when using a mouse or trackpad.'
scrollPastEnd:
type: 'boolean'
default: false
+ description: 'Allow the editor to be scrolled past the end of the last line.'
undoGroupingInterval:
type: 'integer'
default: 300
minimum: 0
- description: 'Time interval in milliseconds within which operations will be grouped together in the undo history'
+ description: 'Time interval in milliseconds within which text editing operations will be grouped together in the undo history.'
useShadowDOM:
type: 'boolean'
default: true
@@ -190,33 +211,39 @@ module.exports =
type: 'boolean'
default: true
title: 'Confirm Checkout HEAD Revision'
+ description: 'Show confirmation dialog when checking out the HEAD revision and discarding changes to current file since last commit.'
backUpBeforeSaving:
type: 'boolean'
default: false
description: 'Ensure file contents aren\'t lost if there is an I/O error during save by making a temporary backup copy.'
invisibles:
type: 'object'
+ description: 'A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false to turn off individual whitespace character types).'
properties:
eol:
type: ['boolean', 'string']
default: '\u00ac'
maximumLength: 1
+ description: 'Character used to render newline characters (\\n) when the `Show Invisibles` setting is enabled. '
space:
type: ['boolean', 'string']
default: '\u00b7'
maximumLength: 1
+ description: 'Character used to render leading and trailing space characters when the `Show Invisibles` setting is enabled.'
tab:
type: ['boolean', 'string']
default: '\u00bb'
maximumLength: 1
+ description: 'Character used to render hard tab characters (\\t) when the `Show Invisibles` setting is enabled.'
cr:
type: ['boolean', 'string']
default: '\u00a4'
maximumLength: 1
+ description: 'Character used to render carriage return characters (for Microsoft-style line endings) when the `Show Invisibles` setting is enabled.'
zoomFontWhenCtrlScrolling:
type: 'boolean'
default: process.platform isnt 'darwin'
- description: 'Increase/decrease the editor font size when pressing the Ctrl key and scrolling the mouse up/down.'
+ description: 'Change the editor font size when pressing the Ctrl key and scrolling the mouse up/down.'
if process.platform in ['win32', 'linux']
module.exports.core.properties.autoHideMenuBar =
diff --git a/src/config.coffee b/src/config.coffee
index 176390869..663dbb777 100644
--- a/src/config.coffee
+++ b/src/config.coffee
@@ -5,7 +5,10 @@ CSON = require 'season'
path = require 'path'
async = require 'async'
pathWatcher = require 'pathwatcher'
-{pushKeyPath, splitKeyPath, getValueAtKeyPath, setValueAtKeyPath} = require 'key-path-helpers'
+{
+ getValueAtKeyPath, setValueAtKeyPath, deleteValueAtKeyPath,
+ pushKeyPath, splitKeyPath,
+} = require 'key-path-helpers'
Color = require './color'
ScopedPropertyStore = require 'scoped-property-store'
@@ -331,7 +334,13 @@ class Config
value
# Created during initialization, available as `atom.config`
- constructor: ({@configDirPath, @resourcePath}={}) ->
+ constructor: ({@configDirPath, @resourcePath, @notificationManager, @enablePersistence}={}) ->
+ if @enablePersistence?
+ @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
+ @configFilePath ?= path.join(@configDirPath, 'config.cson')
+ @clear()
+
+ clear: ->
@emitter = new Emitter
@schema =
type: 'object'
@@ -340,11 +349,8 @@ class Config
@settings = {}
@scopedSettingsStore = new ScopedPropertyStore
@configFileHasErrors = false
- @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
- @configFilePath ?= path.join(@configDirPath, 'config.cson')
@transactDepth = 0
@savePending = false
-
@requestLoad = _.debounce(@loadUserConfig, 100)
@requestSave = =>
@savePending = true
@@ -354,6 +360,8 @@ class Config
@save()
debouncedSave = _.debounce(save, 100)
+ shouldNotAccessFileSystem: -> not @enablePersistence
+
###
Section: Config Subscription
###
@@ -414,7 +422,6 @@ class Config
# * `event` {Object}
# * `newValue` the new value of the key
# * `oldValue` the prior value of the key.
- # * `keyPath` the keyPath of the changed key
#
# Returns a {Disposable} with the following keys on which you can call
# `.dispose()` to unsubscribe.
@@ -724,7 +731,7 @@ class Config
###
initializeConfigDirectory: (done) ->
- return if fs.existsSync(@configDirPath)
+ return if fs.existsSync(@configDirPath) or @shouldNotAccessFileSystem()
fs.makeTreeSync(@configDirPath)
@@ -740,6 +747,8 @@ class Config
fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true)
loadUserConfig: ->
+ return if @shouldNotAccessFileSystem()
+
unless fs.existsSync(@configFilePath)
fs.makeTreeSync(path.dirname(@configFilePath))
CSON.writeFileSync(@configFilePath, {})
@@ -763,6 +772,8 @@ class Config
@notifyFailure(message, detail)
observeUserConfig: ->
+ return if @shouldNotAccessFileSystem()
+
try
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
@requestLoad() if eventType is 'change' and @watchSubscription?
@@ -779,9 +790,11 @@ class Config
@watchSubscription = null
notifyFailure: (errorMessage, detail) ->
- atom.notifications.addError(errorMessage, {detail, dismissable: true})
+ @notificationManager.addError(errorMessage, {detail, dismissable: true})
save: ->
+ return if @shouldNotAccessFileSystem()
+
allSettings = {'*': @settings}
allSettings = _.extend allSettings, @scopedSettingsStore.propertiesForSource(@getUserConfigPath())
try
@@ -832,12 +845,16 @@ class Config
setRawValue: (keyPath, value) ->
defaultValue = getValueAtKeyPath(@defaultSettings, keyPath)
- value = undefined if _.isEqual(defaultValue, value)
-
- if keyPath?
- setValueAtKeyPath(@settings, keyPath, value)
+ if _.isEqual(defaultValue, value)
+ if keyPath?
+ deleteValueAtKeyPath(@settings, keyPath)
+ else
+ @settings = null
else
- @settings = value
+ if keyPath?
+ setValueAtKeyPath(@settings, keyPath, value)
+ else
+ @settings = value
@emitChangeEvent()
observeKeyPath: (keyPath, options, callback) ->
diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee
index 0ca005d8a..869076beb 100644
--- a/src/context-menu-manager.coffee
+++ b/src/context-menu-manager.coffee
@@ -4,6 +4,7 @@ CSON = require 'season'
fs = require 'fs-plus'
{calculateSpecificity, validateSelector} = require 'clear-cut'
{Disposable} = require 'event-kit'
+remote = require 'remote'
MenuHelpers = require './menu-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
@@ -40,11 +41,11 @@ platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
# {::add} for more information.
module.exports =
class ContextMenuManager
- constructor: ({@resourcePath, @devMode}) ->
+ constructor: ({@resourcePath, @devMode, @keymapManager}) ->
@definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data
@clear()
- atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
+ @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems()
loadPlatformItems: ->
if platformContextMenu?
@@ -175,7 +176,7 @@ class ContextMenuManager
menuTemplate = @templateForEvent(event)
return unless menuTemplate?.length > 0
- atom.getCurrentWindow().emit('context-menu', menuTemplate)
+ remote.getCurrentWindow().emit('context-menu', menuTemplate)
return
clear: ->
diff --git a/src/cursor.coffee b/src/cursor.coffee
index 84396448c..40cde4aca 100644
--- a/src/cursor.coffee
+++ b/src/cursor.coffee
@@ -16,7 +16,7 @@ class Cursor extends Model
visible: true
# Instantiated by a {TextEditor}
- constructor: ({@editor, @marker, id}) ->
+ constructor: ({@editor, @marker, @config, id}) ->
@emitter = new Emitter
@assignId(id)
@@ -75,7 +75,7 @@ class Cursor extends Model
@changePosition options, =>
@marker.setHeadScreenPosition(screenPosition, options)
- # Public: Returns the screen position of the cursor as an Array.
+ # Public: Returns the screen position of the cursor as a {Point}.
getScreenPosition: ->
@marker.getHeadScreenPosition()
@@ -158,7 +158,7 @@ class Cursor extends Model
[before, after] = @editor.getTextInBufferRange(range)
return false if /\s/.test(before) or /\s/.test(after)
- nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()).split('')
+ nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()).split('')
_.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after)
# Public: Returns whether this cursor is between a word's start and end.
@@ -605,7 +605,7 @@ class Cursor extends Model
# Returns a {RegExp}.
wordRegExp: ({includeNonWordCharacters}={}) ->
includeNonWordCharacters ?= true
- nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor())
+ nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor())
segments = ["^[\t ]*$"]
segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+")
if includeNonWordCharacters
@@ -620,7 +620,7 @@ class Cursor extends Model
#
# Returns a {RegExp}.
subwordRegExp: (options={}) ->
- nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor())
+ nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor())
lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+"
diff --git a/src/custom-gutter-component.coffee b/src/custom-gutter-component.coffee
index 32710ea46..9fd25fd3f 100644
--- a/src/custom-gutter-component.coffee
+++ b/src/custom-gutter-component.coffee
@@ -6,12 +6,12 @@
module.exports =
class CustomGutterComponent
- constructor: ({@gutter}) ->
+ constructor: ({@gutter, @views}) ->
@decorationNodesById = {}
@decorationItemsById = {}
@visible = true
- @domNode = atom.views.getView(@gutter)
+ @domNode = @views.getView(@gutter)
@decorationsNode = @domNode.firstChild
# Clear the contents in case the domNode is being reused.
@decorationsNode.innerHTML = ''
diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee
index 62c7e4401..7f6cb0f65 100644
--- a/src/deserializer-manager.coffee
+++ b/src/deserializer-manager.coffee
@@ -21,7 +21,7 @@
# ```
module.exports =
class DeserializerManager
- constructor: ->
+ constructor: (@atomEnvironment) ->
@deserializers = {}
# Public: Register the given class(es) as deserializers.
@@ -29,7 +29,10 @@ class DeserializerManager
# * `deserializers` One or more deserializers to register. A deserializer can
# be any object with a `.name` property and a `.deserialize()` method. A
# common approach is to register a *constructor* as the deserializer for its
- # instances by adding a `.deserialize()` class method.
+ # instances by adding a `.deserialize()` class method. When your method is
+ # called, it will be passed serialized state as the first argument and the
+ # {Atom} environment object as the second argument, which is useful if you
+ # wish to avoid referencing the `atom` global.
add: (deserializers...) ->
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
new Disposable =>
@@ -39,15 +42,13 @@ class DeserializerManager
# Public: Deserialize the state and params.
#
# * `state` The state {Object} to deserialize.
- # * `params` The params {Object} to pass as the second arguments to the
- # deserialize method of the deserializer.
- deserialize: (state, params) ->
+ deserialize: (state) ->
return unless state?
if deserializer = @get(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
- deserializer.deserialize(state, params)
+ deserializer.deserialize(state, @atomEnvironment)
else
console.warn "No deserializer found for", state
@@ -59,3 +60,6 @@ class DeserializerManager
name = state.get?('deserializer') ? state.deserializer
@deserializers[name]
+
+ clear: ->
+ @deserializers = {}
diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee
index 339fa11c1..1021955cf 100644
--- a/src/display-buffer.coffee
+++ b/src/display-buffer.coffee
@@ -26,17 +26,29 @@ class DisplayBuffer extends Model
height: null
width: null
- @deserialize: (state) ->
- state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer)
+ @deserialize: (state, atomEnvironment) ->
+ state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
+ state.config = atomEnvironment.config
+ state.assert = atomEnvironment.assert
+ state.grammarRegistry = atomEnvironment.grammars
+ state.packageManager = atomEnvironment.packages
new this(state)
- constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, @largeFileMode}={}) ->
+ constructor: (params={}) ->
super
+ {
+ tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles,
+ @largeFileMode, @config, @assert, @grammarRegistry, @packageManager
+ } = params
+
@emitter = new Emitter
@disposables = new CompositeDisposable
- @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, ignoreInvisibles, @largeFileMode})
+ @tokenizedBuffer ?= new TokenizedBuffer({
+ tabLength, buffer, ignoreInvisibles, @largeFileMode, @config,
+ @grammarRegistry, @packageManager, @assert
+ })
@buffer = @tokenizedBuffer.buffer
@charWidthsByScope = {}
@markers = {}
@@ -61,29 +73,29 @@ class DisplayBuffer extends Model
oldConfigSettings = @configSettings
@configSettings =
- scrollPastEnd: atom.config.get('editor.scrollPastEnd', scope: scopeDescriptor)
- softWrap: atom.config.get('editor.softWrap', scope: scopeDescriptor)
- softWrapAtPreferredLineLength: atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
- softWrapHangingIndent: atom.config.get('editor.softWrapHangingIndent', scope: scopeDescriptor)
- preferredLineLength: atom.config.get('editor.preferredLineLength', scope: scopeDescriptor)
+ scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor)
+ softWrap: @config.get('editor.softWrap', scope: scopeDescriptor)
+ softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
+ softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor)
+ preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor)
- subscriptions.add atom.config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) =>
+ subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrap = newValue
@updateWrappedScreenLines()
- subscriptions.add atom.config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) =>
+ subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrapHangingIndent = newValue
@updateWrappedScreenLines()
- subscriptions.add atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) =>
+ subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrapAtPreferredLineLength = newValue
@updateWrappedScreenLines() if @isSoftWrapped()
- subscriptions.add atom.config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) =>
+ subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) =>
@configSettings.preferredLineLength = newValue
- @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
+ @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
- subscriptions.add atom.config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) =>
+ subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) =>
@configSettings.scrollPastEnd = value
@updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings)
@@ -97,7 +109,10 @@ class DisplayBuffer extends Model
largeFileMode: @largeFileMode
copy: ->
- newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @largeFileMode})
+ newDisplayBuffer = new DisplayBuffer({
+ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
+ @grammarRegistry, @packageManager
+ })
for marker in @findMarkers(displayBufferId: @id)
marker.copy(displayBufferId: newDisplayBuffer.id)
@@ -189,10 +204,24 @@ class DisplayBuffer extends Model
getLineHeightInPixels: -> @lineHeightInPixels
setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
+ getKoreanCharWidth: -> @koreanCharWidth
+
+ getHalfWidthCharWidth: -> @halfWidthCharWidth
+
+ getDoubleWidthCharWidth: -> @doubleWidthCharWidth
+
getDefaultCharWidth: -> @defaultCharWidth
- setDefaultCharWidth: (defaultCharWidth) ->
- if defaultCharWidth isnt @defaultCharWidth
+
+ setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
+ doubleWidthCharWidth ?= defaultCharWidth
+ halfWidthCharWidth ?= defaultCharWidth
+ koreanCharWidth ?= defaultCharWidth
+ if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth
@defaultCharWidth = defaultCharWidth
+ @doubleWidthCharWidth = doubleWidthCharWidth
+ @halfWidthCharWidth = halfWidthCharWidth
+ @koreanCharWidth = koreanCharWidth
+ @updateWrappedScreenLines() if @isSoftWrapped() and @getEditorWidthInChars()?
defaultCharWidth
getCursorWidth: -> 1
@@ -262,6 +291,40 @@ class DisplayBuffer extends Model
else
@getEditorWidthInChars()
+ getSoftWrapColumnForTokenizedLine: (tokenizedLine) ->
+ lineMaxWidth = @getSoftWrapColumn() * @getDefaultCharWidth()
+
+ return if Number.isNaN(lineMaxWidth)
+ return 0 if lineMaxWidth is 0
+
+ iterator = tokenizedLine.getTokenIterator(false)
+ column = 0
+ currentWidth = 0
+ while iterator.next()
+ textIndex = 0
+ text = iterator.getText()
+ while textIndex < text.length
+ if iterator.isPairedCharacter()
+ charLength = 2
+ else
+ charLength = 1
+
+ if iterator.hasDoubleWidthCharacterAt(textIndex)
+ charWidth = @getDoubleWidthCharWidth()
+ else if iterator.hasHalfWidthCharacterAt(textIndex)
+ charWidth = @getHalfWidthCharWidth()
+ else if iterator.hasKoreanCharacterAt(textIndex)
+ charWidth = @getKoreanCharWidth()
+ else
+ charWidth = @getDefaultCharWidth()
+
+ return column if currentWidth + charWidth > lineMaxWidth
+
+ currentWidth += charWidth
+ column += charLength
+ textIndex += charLength
+ column
+
# Gets the screen line for the given screen row.
#
# * `screenRow` - A {Number} indicating the screen row.
@@ -958,7 +1021,7 @@ class DisplayBuffer extends Model
else
softWraps = 0
if @isSoftWrapped()
- while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumn())
+ while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine))
[wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(
wrapScreenColumn,
@configSettings.softWrapHangingIndent
@@ -1036,8 +1099,8 @@ class DisplayBuffer extends Model
tokenizedLinesCount = @tokenizedBuffer.getLineCount()
bufferLinesCount = @buffer.getLineCount()
- atom.assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) ->
+ @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) ->
error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
- atom.assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) ->
+ @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) ->
error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee
index 3b4df5d2b..593324d0c 100644
--- a/src/git-repository-provider.coffee
+++ b/src/git-repository-provider.coffee
@@ -48,7 +48,7 @@ isValidGitDirectorySync = (directory) ->
module.exports =
class GitRepositoryProvider
- constructor: (@project) ->
+ constructor: (@project, @config) ->
# Keys are real paths that end in `.git`.
# Values are the corresponding GitRepository objects.
@pathToRepository = {}
@@ -75,7 +75,7 @@ class GitRepositoryProvider
gitDirPath = gitDir.getPath()
repo = @pathToRepository[gitDirPath]
unless repo
- repo = GitRepository.open(gitDirPath, project: @project)
+ repo = GitRepository.open(gitDirPath, {@project, @config})
return null unless repo
repo.onDidDestroy(=> delete @pathToRepository[gitDirPath])
@pathToRepository[gitDirPath] = repo
diff --git a/src/git-repository.coffee b/src/git-repository.coffee
index 064b86dd3..1663f9ad4 100644
--- a/src/git-repository.coffee
+++ b/src/git-repository.coffee
@@ -80,7 +80,7 @@ class GitRepository
for submodulePath, submoduleRepo of @repo.submodules
submoduleRepo.upstream = {ahead: 0, behind: 0}
- {@project, refreshOnWindowFocus} = options
+ {@project, @config, refreshOnWindowFocus} = options
refreshOnWindowFocus ?= true
if refreshOnWindowFocus
@@ -443,25 +443,10 @@ class GitRepository
# Subscribes to editor view event.
checkoutHeadForEditor: (editor) ->
- filePath = editor.getPath()
- return unless filePath
-
- fileName = basename(filePath)
-
- checkoutHead = =>
+ if filePath = editor.getPath()
editor.buffer.reload() if editor.buffer.isModified()
@checkoutHead(filePath)
- if atom.config.get('editor.confirmCheckoutHeadRevision')
- atom.confirm
- message: 'Confirm Checkout HEAD Revision'
- detailedMessage: "Are you sure you want to discard all changes to \"#{fileName}\" since the last Git commit?"
- buttons:
- OK: checkoutHead
- Cancel: null
- else
- checkoutHead()
-
# Returns the corresponding {Repository}
getRepo: (path) ->
if @repo?
diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee
index 109e719fc..4a5352275 100644
--- a/src/grammar-registry.coffee
+++ b/src/grammar-registry.coffee
@@ -14,19 +14,9 @@ PathSplitRegex = new RegExp("[/.]")
# language-specific comment regexes. See {::getProperty} for more details.
module.exports =
class GrammarRegistry extends FirstMate.GrammarRegistry
- @deserialize: ({grammarOverridesByPath}) ->
- grammarRegistry = new GrammarRegistry()
- grammarRegistry.grammarOverridesByPath = grammarOverridesByPath
- grammarRegistry
-
- atom.deserializers.add(this)
-
- constructor: ->
+ constructor: ({@config}={}) ->
super(maxTokensPerLine: 100)
- serialize: ->
- {deserializer: @constructor.name, @grammarOverridesByPath}
-
createToken: (value, scopes) -> new Token({value, scopes})
# Extended: Select a grammar for the given file path and file contents.
@@ -70,7 +60,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
pathScore = -1
fileTypes = grammar.fileTypes
- if customFileTypes = atom.config.get('core.customFileTypes')?[grammar.scopeName]
+ if customFileTypes = @config.get('core.customFileTypes')?[grammar.scopeName]
fileTypes = fileTypes.concat(customFileTypes)
for fileType, i in fileTypes
@@ -133,6 +123,3 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
clearGrammarOverrides: ->
@grammarOverridesByPath = {}
undefined
-
- clearObservers: ->
- @emitter = new Emitter
diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee
index 40e2c8c26..c5538324b 100644
--- a/src/gutter-container-component.coffee
+++ b/src/gutter-container-component.coffee
@@ -7,7 +7,7 @@ LineNumberGutterComponent = require './line-number-gutter-component'
module.exports =
class GutterContainerComponent
- constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool}) ->
+ constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) ->
# An array of objects of the form: {name: {String}, component: {Object}}
@gutterComponents = []
@gutterComponentsByGutterName = {}
@@ -39,10 +39,10 @@ class GutterContainerComponent
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
- gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool})
+ gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views})
@lineNumberGutterComponent = gutterComponent
else
- gutterComponent = new CustomGutterComponent({gutter})
+ gutterComponent = new CustomGutterComponent({gutter, @views})
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
# Pass the gutter only the state that it needs.
diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee
new file mode 100644
index 000000000..acf50bc5d
--- /dev/null
+++ b/src/initialize-application-window.coffee
@@ -0,0 +1,34 @@
+# Like sands through the hourglass, so are the days of our lives.
+
+path = require 'path'
+require './window'
+{getWindowLoadSettings} = require './window-load-settings-helpers'
+
+{resourcePath, isSpec, devMode} = getWindowLoadSettings()
+
+# Add application-specific exports to module search path.
+exportsPath = path.join(resourcePath, 'exports')
+require('module').globalPaths.push(exportsPath)
+process.env.NODE_PATH = exportsPath
+
+# Make React faster
+process.env.NODE_ENV ?= 'production' unless devMode
+
+AtomEnvironment = require './atom-environment'
+ApplicationDelegate = require './application-delegate'
+window.atom = new AtomEnvironment({
+ window, document,
+ applicationDelegate: new ApplicationDelegate,
+ configDirPath: process.env.ATOM_HOME
+ enablePersistence: true
+})
+
+atom.displayWindow()
+atom.loadStateSync()
+atom.startEditorWindow()
+
+# Workaround for focus getting cleared upon window creation
+windowFocused = ->
+ window.removeEventListener('focus', windowFocused)
+ setTimeout (-> document.querySelector('atom-workspace').focus()), 0
+window.addEventListener('focus', windowFocused)
diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee
new file mode 100644
index 000000000..27ef46acc
--- /dev/null
+++ b/src/initialize-test-window.coffee
@@ -0,0 +1,69 @@
+# Start the crash reporter before anything else.
+require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
+remote = require 'remote'
+
+exitWithStatusCode = (status) ->
+ remote.require('app').emit('will-quit')
+ remote.process.exit(status)
+
+try
+ path = require 'path'
+ ipc = require 'ipc'
+ {getWindowLoadSettings} = require './window-load-settings-helpers'
+ AtomEnvironment = require '../src/atom-environment'
+ ApplicationDelegate = require '../src/application-delegate'
+
+ {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings()
+
+ if headless
+ # Override logging in headless mode so it goes to the console, regardless
+ # of the --enable-logging flag to Electron.
+ console.log = (args...) ->
+ ipc.send 'write-to-stdout', args.join(' ') + '\n'
+ console.warn = (args...) ->
+ ipc.send 'write-to-stderr', args.join(' ') + '\n'
+ console.error = (args...) ->
+ ipc.send 'write-to-stderr', args.join(' ') + '\n'
+ else
+ # Show window synchronously so a focusout doesn't fire on input elements
+ # that are focused in the very first spec run.
+ remote.getCurrentWindow().show()
+
+ handleKeydown = (event) ->
+ # Reload: cmd-r / ctrl-r
+ if (event.metaKey or event.ctrlKey) and event.keyCode is 82
+ ipc.send('call-window-method', 'restart')
+
+ # Toggle Dev Tools: cmd-alt-i / ctrl-alt-i
+ if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73
+ ipc.send('call-window-method', 'toggleDevTools')
+
+ # Reload: cmd-w / ctrl-w
+ if (event.metaKey or event.ctrlKey) and event.keyCode is 87
+ ipc.send('call-window-method', 'close')
+
+ window.addEventListener('keydown', handleKeydown, true)
+
+ # Add 'exports' to module search path.
+ exportsPath = path.join(getWindowLoadSettings().resourcePath, 'exports')
+ require('module').globalPaths.push(exportsPath)
+ process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it.
+
+ document.title = "Spec Suite"
+
+ testRunner = require(testRunnerPath)
+ legacyTestRunner = require(legacyTestRunnerPath)
+ buildAtomEnvironment = (params) -> new AtomEnvironment(params)
+ buildDefaultApplicationDelegate = (params) -> new ApplicationDelegate()
+
+ promise = testRunner({
+ logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
+ })
+
+ promise.then(exitWithStatusCode) if getWindowLoadSettings().headless
+catch error
+ if getWindowLoadSettings().headless
+ console.error(error.stack ? error)
+ exitWithStatusCode(1)
+ else
+ throw error
diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee
index da77f0156..82f2e8b99 100644
--- a/src/keymap-extensions.coffee
+++ b/src/keymap-extensions.coffee
@@ -20,6 +20,8 @@ KeymapManager::loadBundledKeymaps = ->
@emitter.emit 'did-load-bundled-keymaps'
KeymapManager::getUserKeymapPath = ->
+ return "" unless @configDirPath?
+
if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
userKeymapPath
else
@@ -41,11 +43,11 @@ KeymapManager::loadUserKeymap = ->
[this document][watches] for more info.
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
"""
- atom.notifications.addError(message, {dismissable: true})
+ @notificationManager.addError(message, {dismissable: true})
else
detail = error.path
stack = error.stack
- atom.notifications.addFatalError(error.message, {detail, stack, dismissable: true})
+ @notificationManager.addFatalError(error.message, {detail, stack, dismissable: true})
KeymapManager::subscribeToFileReadFailure = ->
@onDidFailToReadFile (error) =>
@@ -57,6 +59,6 @@ KeymapManager::subscribeToFileReadFailure = ->
else
error.message
- atom.notifications.addError(message, {detail, dismissable: true})
+ @notificationManager.addError(message, {detail, dismissable: true})
module.exports = KeymapManager
diff --git a/src/language-mode.coffee b/src/language-mode.coffee
index e60e85f7f..55707bad3 100644
--- a/src/language-mode.coffee
+++ b/src/language-mode.coffee
@@ -8,7 +8,7 @@ class LanguageMode
# Sets up a `LanguageMode` for the given {TextEditor}.
#
# editor - The {TextEditor} to associate with
- constructor: (@editor) ->
+ constructor: (@editor, @config) ->
{@buffer} = @editor
destroy: ->
@@ -327,7 +327,7 @@ class LanguageMode
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
getRegexForProperty: (scopeDescriptor, property) ->
- if pattern = atom.config.get(property, scope: scopeDescriptor)
+ if pattern = @config.get(property, scope: scopeDescriptor)
new OnigRegExp(pattern)
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@@ -343,8 +343,8 @@ class LanguageMode
@getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern')
commentStartAndEndStringsForScope: (scope) ->
- commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0]
- commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) ->
+ commentStartEntry = @config.getAll('editor.commentStart', {scope})[0]
+ commentEndEntry = _.find @config.getAll('editor.commentEnd', {scope}), (entry) ->
entry.scopeSelector is commentStartEntry.scopeSelector
commentStartString = commentStartEntry?.value
commentEndString = commentEndEntry?.value
diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee
index b4a5bc168..bb66ff144 100644
--- a/src/line-number-gutter-component.coffee
+++ b/src/line-number-gutter-component.coffee
@@ -7,12 +7,12 @@ module.exports =
class LineNumberGutterComponent extends TiledComponent
dummyLineNumberNode: null
- constructor: ({@onMouseDown, @editor, @gutter, @domElementPool}) ->
+ constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) ->
@visible = true
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
- @domNode = atom.views.getView(@gutter)
+ @domNode = @views.getView(@gutter)
@lineNumbersNode = @domNode.firstChild
@lineNumbersNode.innerHTML = ''
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
index 2a721a0aa..b5af56885 100644
--- a/src/lines-component.coffee
+++ b/src/lines-component.coffee
@@ -7,13 +7,21 @@ DummyLineNode.className = 'line'
DummyLineNode.style.position = 'absolute'
DummyLineNode.style.visibility = 'hidden'
DummyLineNode.appendChild(document.createElement('span'))
-DummyLineNode.firstChild.textContent = 'x'
+DummyLineNode.appendChild(document.createElement('span'))
+DummyLineNode.appendChild(document.createElement('span'))
+DummyLineNode.appendChild(document.createElement('span'))
+DummyLineNode.children[0].textContent = 'x'
+DummyLineNode.children[1].textContent = '我'
+DummyLineNode.children[2].textContent = 'ハ'
+DummyLineNode.children[3].textContent = '세'
+
+RangeForMeasurement = document.createRange()
module.exports =
class LinesComponent extends TiledComponent
placeholderTextDiv: null
- constructor: ({@presenter, @useShadowDOM, @domElementPool}) ->
+ constructor: ({@presenter, @useShadowDOM, @domElementPool, @assert, @grammars}) ->
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@tilesNode = document.createElement("div")
@@ -64,7 +72,7 @@ class LinesComponent extends TiledComponent
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
- buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool})
+ buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
buildEmptyState: ->
{tiles: {}}
@@ -76,12 +84,18 @@ class LinesComponent extends TiledComponent
measureLineHeightAndDefaultCharWidth: ->
@domNode.appendChild(DummyLineNode)
+ textNode = DummyLineNode.firstChild.childNodes[0]
+
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
- charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
+ defaultCharWidth = DummyLineNode.children[0].getBoundingClientRect().width
+ doubleWidthCharWidth = DummyLineNode.children[1].getBoundingClientRect().width
+ halfWidthCharWidth = DummyLineNode.children[2].getBoundingClientRect().width
+ koreanCharWidth = DummyLineNode.children[3].getBoundingClientRect().width
+
@domNode.removeChild(DummyLineNode)
@presenter.setLineHeight(lineHeightInPixels)
- @presenter.setBaseCharacterWidth(charWidth)
+ @presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
tile = @presenter.tileForRow(screenRow)
diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee
index 619acc56a..6b4ac80ba 100644
--- a/src/lines-tile-component.coffee
+++ b/src/lines-tile-component.coffee
@@ -13,8 +13,8 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
- constructor: ({@presenter, @id, @domElementPool}) ->
- @tokenIterator = new TokenIterator
+ constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) ->
+ @tokenIterator = new TokenIterator(grammarRegistry: grammars)
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee
index 0dc3e7602..54ba6cf57 100644
--- a/src/lines-yardstick.coffee
+++ b/src/lines-yardstick.coffee
@@ -3,8 +3,8 @@ TokenIterator = require './token-iterator'
module.exports =
class LinesYardstick
- constructor: (@model, @presenter, @lineNodesProvider) ->
- @tokenIterator = new TokenIterator
+ constructor: (@model, @presenter, @lineNodesProvider, grammarRegistry) ->
+ @tokenIterator = new TokenIterator({grammarRegistry})
@rangeForMeasurement = document.createRange()
@invalidateCache()
@@ -40,7 +40,7 @@ class LinesYardstick
previousColumn = 0
previousLeft = 0
- @tokenIterator.reset(line)
+ @tokenIterator.reset(line, false)
while @tokenIterator.next()
text = @tokenIterator.getText()
textIndex = 0
@@ -112,7 +112,7 @@ class LinesYardstick
indexWithinTextNode = null
charIndex = 0
- @tokenIterator.reset(line)
+ @tokenIterator.reset(line, false)
while @tokenIterator.next()
break if foundIndexWithinTextNode?
@@ -159,9 +159,12 @@ class LinesYardstick
0
leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) ->
- @rangeForMeasurement.setStart(textNode, 0)
- @rangeForMeasurement.setEnd(textNode, charIndex)
- width = @rangeForMeasurement.getBoundingClientRect().width
+ if charIndex is 0
+ width = 0
+ else
+ @rangeForMeasurement.setStart(textNode, 0)
+ @rangeForMeasurement.setEnd(textNode, charIndex)
+ width = @rangeForMeasurement.getBoundingClientRect().width
@rangeForMeasurement.setStart(textNode, 0)
@rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee
index b138f2da2..fa78d3cd6 100644
--- a/src/menu-manager.coffee
+++ b/src/menu-manager.coffee
@@ -59,12 +59,12 @@ platformMenu = require('../package.json')?._atomMenu?.menu
# See {::add} for more info about adding menu's directly.
module.exports =
class MenuManager
- constructor: ({@resourcePath}) ->
+ constructor: ({@resourcePath, @keymapManager, @packageManager}) ->
@pendingUpdateOperation = null
@template = []
- atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
- atom.keymaps.onDidReloadKeymap => @update()
- atom.packages.onDidActivateInitialPackages => @sortPackagesMenu()
+ @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems()
+ @keymapManager.onDidReloadKeymap => @update()
+ @packageManager.onDidActivateInitialPackages => @sortPackagesMenu()
# Public: Adds the given items to the application menu.
#
@@ -96,6 +96,10 @@ class MenuManager
@unmerge(@template, item) for item in items
@update()
+ clear: ->
+ @template = []
+ @update()
+
# Should the binding for the given selector be included in the menu
# commands.
#
@@ -143,7 +147,7 @@ class MenuManager
includedBindings = []
unsetKeystrokes = new Set
- for binding in atom.keymaps.getKeyBindings() when @includeSelector(binding.selector)
+ for binding in @keymapManager.getKeyBindings() when @includeSelector(binding.selector)
includedBindings.push(binding)
if binding.command is 'unset!'
unsetKeystrokes.add(binding.keystrokes)
@@ -191,7 +195,10 @@ class MenuManager
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
- element?.classList.toString().split(' ') ? []
+ if classList = element?.classList
+ Array::slice.apply(classList)
+ else
+ []
sortPackagesMenu: ->
packagesMenu = _.find @template, ({label}) -> MenuHelpers.normalizeLabel(label) is 'Packages'
diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee
index 8f790e0c6..46c781c20 100644
--- a/src/notification-manager.coffee
+++ b/src/notification-manager.coffee
@@ -29,40 +29,65 @@ class NotificationManager
# Public: Add a success notification.
#
# * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
+ # * `options` (optional) An options {Object} with the following keys:
+ # * `detail` (optional) A {String} with additional details about the
+ # notification.
+ # * `dismissable` (optional) A {Boolean} indicating whether this
+ # notification can be dismissed by the user. Defaults to `false`.
+ # * `icon` (optional) A {String} name of an icon from Octicons to display
+ # in the notification header. Defaults to `'check'`.
addSuccess: (message, options) ->
@addNotification(new Notification('success', message, options))
# Public: Add an informational notification.
#
# * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
+ # * `options` (optional) An options {Object} with the following keys:
+ # * `detail` (optional) A {String} with additional details about the
+ # notification.
+ # * `dismissable` (optional) A {Boolean} indicating whether this
+ # notification can be dismissed by the user. Defaults to `false`.
+ # * `icon` (optional) A {String} name of an icon from Octicons to display
+ # in the notification header. Defaults to `'info'`.
addInfo: (message, options) ->
@addNotification(new Notification('info', message, options))
# Public: Add a warning notification.
#
# * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
+ # * `options` (optional) An options {Object} with the following keys:
+ # * `detail` (optional) A {String} with additional details about the
+ # notification.
+ # * `dismissable` (optional) A {Boolean} indicating whether this
+ # notification can be dismissed by the user. Defaults to `false`.
+ # * `icon` (optional) A {String} name of an icon from Octicons to display
+ # in the notification header. Defaults to `'alert'`.
addWarning: (message, options) ->
@addNotification(new Notification('warning', message, options))
# Public: Add an error notification.
#
# * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
+ # * `options` (optional) An options {Object} with the following keys:
+ # * `detail` (optional) A {String} with additional details about the
+ # notification.
+ # * `dismissable` (optional) A {Boolean} indicating whether this
+ # notification can be dismissed by the user. Defaults to `false`.
+ # * `icon` (optional) A {String} name of an icon from Octicons to display
+ # in the notification header. Defaults to `'flame'`.
addError: (message, options) ->
@addNotification(new Notification('error', message, options))
# Public: Add a fatal error notification.
#
# * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
+ # * `options` (optional) An options {Object} with the following keys:
+ # * `detail` (optional) A {String} with additional details about the
+ # notification.
+ # * `dismissable` (optional) A {Boolean} indicating whether this
+ # notification can be dismissed by the user. Defaults to `false`.
+ # * `icon` (optional) A {String} name of an icon from Octicons to display
+ # in the notification header. Defaults to `'bug'`.
addFatalError: (message, options) ->
@addNotification(new Notification('fatal', message, options))
diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee
index 7858cb8a2..83787ad10 100644
--- a/src/overlay-manager.coffee
+++ b/src/overlay-manager.coffee
@@ -1,6 +1,6 @@
module.exports =
class OverlayManager
- constructor: (@presenter, @container) ->
+ constructor: (@presenter, @container, @views) ->
@overlaysById = {}
render: (state) ->
@@ -28,7 +28,7 @@ class OverlayManager
@presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin)
renderOverlay: (state, decorationId, {item, pixelPosition, class: klass}) ->
- itemView = atom.views.getView(item)
+ itemView = @views.getView(item)
cachedOverlay = @overlaysById[decorationId]
unless overlayNode = cachedOverlay?.overlayNode
overlayNode = document.createElement('atom-overlay')
diff --git a/src/package-manager.coffee b/src/package-manager.coffee
index 7e4a19470..f6cef9465 100644
--- a/src/package-manager.coffee
+++ b/src/package-manager.coffee
@@ -1,8 +1,10 @@
path = require 'path'
+normalizePackageData = null
_ = require 'underscore-plus'
{Emitter} = require 'event-kit'
fs = require 'fs-plus'
+CSON = require 'season'
ServiceHub = require 'service-hub'
Package = require './package'
@@ -26,15 +28,21 @@ ThemePackage = require './theme-package'
# settings and also by calling `enablePackage()/disablePackage()`.
module.exports =
class PackageManager
- constructor: ({configDirPath, @devMode, safeMode, @resourcePath}) ->
+ constructor: (params) ->
+ {
+ configDirPath, @devMode, safeMode, @resourcePath, @config, @styleManager,
+ @notificationManager, @keymapManager, @commandRegistry, @grammarRegistry
+ } = params
+
@emitter = new Emitter
@activationHookEmitter = new Emitter
@packageDirPaths = []
- unless safeMode
+ if configDirPath? and not safeMode
if @devMode
@packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
@packageDirPaths.push(path.join(configDirPath, "packages"))
+ @packagesCache = require('../package.json')?._atomPackages ? {}
@loadedPackages = {}
@activePackages = {}
@packageStates = {}
@@ -43,6 +51,17 @@ class PackageManager
@packageActivators = []
@registerPackageActivator(this, ['atom', 'textmate'])
+ setContextMenuManager: (@contextMenuManager) ->
+
+ setMenuManager: (@menuManager) ->
+
+ setThemeManager: (@themeManager) ->
+
+ reset: ->
+ @serviceHub.clear()
+ @deactivatePackages()
+ @packageStates = {}
+
###
Section: Event Subscription
###
@@ -185,7 +204,7 @@ class PackageManager
#
# Returns a {Boolean}.
isPackageDisabled: (name) ->
- _.include(atom.config.get('core.disabledPackages') ? [], name)
+ _.include(@config.get('core.disabledPackages') ? [], name)
###
Section: Accessing active packages
@@ -269,7 +288,7 @@ class PackageManager
packages = []
for packagePath in @getAvailablePackagePaths()
name = path.basename(packagePath)
- metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
+ metadata = @getLoadedPackage(name)?.metadata ? @loadPackageMetadata(packagePath, true)
packages.push(metadata)
packages
@@ -292,7 +311,7 @@ class PackageManager
@packageDependencies
hasAtomEngine: (packagePath) ->
- metadata = Package.loadMetadata(packagePath, true)
+ metadata = @loadPackageMetadata(packagePath, true)
metadata?.engines?.atom?
unobserveDisabledPackages: ->
@@ -300,7 +319,7 @@ class PackageManager
@disabledPackagesSubscription = null
observeDisabledPackages: ->
- @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
+ @disabledPackagesSubscription ?= @config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
packagesToEnable = _.difference(oldValue, newValue)
packagesToDisable = _.difference(newValue, oldValue)
@@ -313,7 +332,7 @@ class PackageManager
@packagesWithKeymapsDisabledSubscription = null
observePackagesWithKeymapsDisabled: ->
- @packagesWithKeymapsDisabledSubscription ?= atom.config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) =>
+ @packagesWithKeymapsDisabledSubscription ?= @config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) =>
keymapsToEnable = _.difference(oldValue, newValue)
keymapsToDisable = _.difference(newValue, oldValue)
@@ -340,7 +359,7 @@ class PackageManager
return pack if pack = @getLoadedPackage(name)
try
- metadata = Package.loadMetadata(packagePath) ? {}
+ metadata = @loadPackageMetadata(packagePath) ? {}
catch error
@handleMetadataError(error, packagePath)
return null
@@ -350,10 +369,15 @@ class PackageManager
console.warn "Could not load #{metadata.name}@#{metadata.version} because it uses deprecated APIs that have been removed."
return null
+ options = {
+ path: packagePath, metadata, packageManager: this, @config, @styleManager,
+ @commandRegistry, @keymapManager, @devMode, @notificationManager,
+ @grammarRegistry, @themeManager, @menuManager, @contextMenuManager
+ }
if metadata.theme
- pack = new ThemePackage(packagePath, metadata)
+ pack = new ThemePackage(options)
else
- pack = new Package(packagePath, metadata)
+ pack = new Package(options)
pack.load()
@loadedPackages[pack.name] = pack
@emitter.emit 'did-load-package', pack
@@ -392,7 +416,7 @@ class PackageManager
activatePackages: (packages) ->
promises = []
- atom.config.transact =>
+ @config.transact =>
for pack in packages
promise = @activatePackage(pack.name)
promises.push(promise) unless pack.hasActivationCommands()
@@ -423,7 +447,7 @@ class PackageManager
# Deactivate all packages
deactivatePackages: ->
- atom.config.transact =>
+ @config.transact =>
@deactivatePackage(pack.name) for pack in @getLoadedPackages()
return
@unobserveDisabledPackages()
@@ -443,7 +467,7 @@ class PackageManager
detail = "#{error.message} in #{metadataPath}"
stack = "#{error.stack}\n at #{metadataPath}:1:1"
message = "Failed to load the #{path.basename(packagePath)} package"
- atom.notifications.addError(message, {stack, detail, dismissable: true})
+ @notificationManager.addError(message, {stack, detail, dismissable: true})
uninstallDirectory: (directory) ->
symlinkPromise = new Promise (resolve) ->
@@ -456,3 +480,40 @@ class PackageManager
[isSymLink, isDir] = values
if not isSymLink and isDir
fs.remove directory, ->
+
+ reloadActivePackageStyleSheets: ->
+ for pack in @getActivePackages() when pack.getType() isnt 'theme'
+ pack.reloadStylesheets?()
+ return
+
+ isBundledPackagePath: (packagePath) ->
+ if @devMode
+ return false unless @resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
+
+ @resourcePathWithTrailingSlash ?= "#{@resourcePath}#{path.sep}"
+ packagePath?.startsWith(@resourcePathWithTrailingSlash)
+
+ loadPackageMetadata: (packagePath, ignoreErrors=false) ->
+ packageName = path.basename(packagePath)
+ if @isBundledPackagePath(packagePath)
+ metadata = @packagesCache[packageName]?.metadata
+ unless metadata?
+ if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
+ try
+ metadata = CSON.readFileSync(metadataPath)
+ @normalizePackageMetadata(metadata)
+ catch error
+ throw error unless ignoreErrors
+
+ metadata ?= {}
+ unless typeof metadata.name is 'string' and metadata.name.length > 0
+ metadata.name = packageName
+
+ metadata
+
+ normalizePackageMetadata: (metadata) ->
+ unless metadata?._id
+ normalizePackageData ?= require 'normalize-package-data'
+ normalizePackageData(metadata)
+ if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
+ metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
diff --git a/src/package.coffee b/src/package.coffee
index 0163f8bcd..00963ef28 100644
--- a/src/package.coffee
+++ b/src/package.coffee
@@ -1,5 +1,4 @@
path = require 'path'
-normalizePackageData = null
_ = require 'underscore-plus'
async = require 'async'
@@ -11,44 +10,10 @@ ModuleCache = require './module-cache'
ScopedProperties = require './scoped-properties'
BufferedProcess = require './buffered-process'
-packagesCache = require('../package.json')?._atomPackages ? {}
-
# Extended: Loads and activates a package's main module and resources such as
# stylesheets, keymaps, grammar, editor properties, and menus.
module.exports =
class Package
- @isBundledPackagePath: (packagePath) ->
- if atom.packages.devMode
- return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
-
- @resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}"
- packagePath?.startsWith(@resourcePathWithTrailingSlash)
-
- @normalizeMetadata: (metadata) ->
- unless metadata?._id
- normalizePackageData ?= require 'normalize-package-data'
- normalizePackageData(metadata)
- if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
- metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
-
- @loadMetadata: (packagePath, ignoreErrors=false) ->
- packageName = path.basename(packagePath)
- if @isBundledPackagePath(packagePath)
- metadata = packagesCache[packageName]?.metadata
- unless metadata?
- if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
- try
- metadata = CSON.readFileSync(metadataPath)
- @normalizeMetadata(metadata)
- catch error
- throw error unless ignoreErrors
-
- metadata ?= {}
- unless typeof metadata.name is 'string' and metadata.name.length > 0
- metadata.name = packageName
-
- metadata
-
keymaps: null
menus: null
stylesheets: null
@@ -64,10 +29,16 @@ class Package
Section: Construction
###
- constructor: (@path, @metadata) ->
+ constructor: (params) ->
+ {
+ @path, @metadata, @packageManager, @config, @styleManager, @commandRegistry,
+ @keymapManager, @devMode, @notificationManager, @grammarRegistry, @themeManager,
+ @menuManager, @contextMenuManager
+ } = params
+
@emitter = new Emitter
- @metadata ?= Package.loadMetadata(@path)
- @bundledPackage = Package.isBundledPackagePath(@path)
+ @metadata ?= @packageManager.loadPackageMetadata(@path)
+ @bundledPackage = @packageManager.isBundledPackagePath(@path)
@name = @metadata?.name ? path.basename(@path)
ModuleCache.add(@path, @metadata)
@reset()
@@ -89,10 +60,10 @@ class Package
###
enable: ->
- atom.config.removeAtKeyPath('core.disabledPackages', @name)
+ @config.removeAtKeyPath('core.disabledPackages', @name)
disable: ->
- atom.config.pushAtKeyPath('core.disabledPackages', @name)
+ @config.pushAtKeyPath('core.disabledPackages', @name)
isTheme: ->
@metadata?.theme?
@@ -132,7 +103,6 @@ class Package
@activationPromise ?=
new Promise (resolve, reject) =>
@resolveActivationPromise = resolve
- @rejectActivationPromise = reject
@measure 'activateTime', =>
try
@activateResources()
@@ -150,7 +120,7 @@ class Package
@activateConfig()
@activateStylesheets()
if @mainModule? and not @mainActivated
- @mainModule.activate?(atom.packages.getPackageState(@name) ? {})
+ @mainModule.activate?(@packageManager.getPackageState(@name) ? {})
@mainActivated = true
@activateServices()
catch error
@@ -164,7 +134,7 @@ class Package
@requireMainModule() unless @mainModule?
if @mainModule?
if @mainModule.config? and typeof @mainModule.config is 'object'
- atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
+ @config.setSchema @name, {type: 'object', properties: @mainModule.config}
@mainModule.activateConfig?()
@configActivated = true
@@ -182,13 +152,13 @@ class Package
else
context = undefined
- @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, priority, context}))
+ @stylesheetDisposables.add(@styleManager.addStyleSheet(source, {sourcePath, priority, context}))
@stylesheetsActivated = true
activateResources: ->
@activationDisposables = new CompositeDisposable
- keymapIsDisabled = _.include(atom.config.get("core.packagesWithKeymapsDisabled") ? [], @name)
+ keymapIsDisabled = _.include(@config.get("core.packagesWithKeymapsDisabled") ? [], @name)
if keymapIsDisabled
@deactivateKeymaps()
else
@@ -197,14 +167,14 @@ class Package
for [menuPath, map] in @menus when map['context-menu']?
try
itemsBySelector = map['context-menu']
- @activationDisposables.add(atom.contextMenu.add(itemsBySelector))
+ @activationDisposables.add(@contextMenuManager.add(itemsBySelector))
catch error
if error.code is 'EBADSELECTOR'
error.message += " in #{menuPath}"
error.stack += "\n at #{menuPath}:1:1"
throw error
- @activationDisposables.add(atom.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
+ @activationDisposables.add(@menuManager.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
unless @grammarsActivated
grammar.activate() for grammar in @grammars
@@ -218,8 +188,8 @@ class Package
@keymapDisposables = new CompositeDisposable()
- @keymapDisposables.add(atom.keymaps.add(keymapPath, map)) for [keymapPath, map] in @keymaps
- atom.menu.update()
+ @keymapDisposables.add(@keymapManager.add(keymapPath, map)) for [keymapPath, map] in @keymaps
+ @menuManager.update()
@keymapActivated = true
@@ -227,7 +197,7 @@ class Package
return if not @keymapActivated
@keymapDisposables?.dispose()
- atom.menu.update()
+ @menuManager.update()
@keymapActivated = false
@@ -243,24 +213,24 @@ class Package
for version, methodName of versions
if typeof @mainModule[methodName] is 'function'
servicesByVersion[version] = @mainModule[methodName]()
- @activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion)
+ @activationDisposables.add @packageManager.serviceHub.provide(name, servicesByVersion)
for name, {versions} of @metadata.consumedServices
for version, methodName of versions
if typeof @mainModule[methodName] is 'function'
- @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
+ @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
return
loadKeymaps: ->
- if @bundledPackage and packagesCache[@name]?
- @keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
+ if @bundledPackage and @packageManager.packagesCache[@name]?
+ @keymaps = (["#{@packageManager.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps)
else
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
return
loadMenus: ->
- if @bundledPackage and packagesCache[@name]?
- @menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus)
+ if @bundledPackage and @packageManager.packagesCache[@name]?
+ @menus = (["#{@packageManager.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of @packageManager.packagesCache[@name].menus)
else
@menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}]
return
@@ -280,8 +250,8 @@ class Package
fs.listSync(menusDirPath, ['cson', 'json'])
loadStylesheets: ->
- @stylesheets = @getStylesheetPaths().map (stylesheetPath) ->
- [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)]
+ @stylesheets = @getStylesheetPaths().map (stylesheetPath) =>
+ [stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)]
getStylesheetsPath: ->
path.join(@path, 'styles')
@@ -304,7 +274,7 @@ class Package
grammarPaths = fs.listSync(grammarsDirPath, ['json', 'cson'])
for grammarPath in grammarPaths
try
- grammar = atom.grammars.readGrammarSync(grammarPath)
+ grammar = @grammarRegistry.readGrammarSync(grammarPath)
grammar.packageName = @name
grammar.bundledPackage = @bundledPackage
@grammars.push(grammar)
@@ -319,11 +289,11 @@ class Package
return Promise.resolve() if @grammarsLoaded
loadGrammar = (grammarPath, callback) =>
- atom.grammars.readGrammar grammarPath, (error, grammar) =>
+ @grammarRegistry.readGrammar grammarPath, (error, grammar) =>
if error?
detail = "#{error.message} in #{grammarPath}"
stack = "#{error.stack}\n at #{grammarPath}:1:1"
- atom.notifications.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true})
+ @notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true})
else
grammar.packageName = @name
grammar.bundledPackage = @bundledPackage
@@ -343,11 +313,11 @@ class Package
@settings = []
loadSettingsFile = (settingsPath, callback) =>
- ScopedProperties.load settingsPath, (error, settings) =>
+ ScopedProperties.load settingsPath, @config, (error, settings) =>
if error?
detail = "#{error.message} in #{settingsPath}"
stack = "#{error.stack}\n at #{settingsPath}:1:1"
- atom.notifications.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true})
+ @notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true})
else
@settings.push(settings)
settings.activate() if @settingsActivated
@@ -370,10 +340,8 @@ class Package
console.error "Error serializing package '#{@name}'", e.stack
deactivate: ->
- @rejectActivationPromise?()
@activationPromise = null
@resolveActivationPromise = null
- @rejectActivationPromise = null
@activationCommandSubscriptions?.dispose()
@deactivateResources()
@deactivateConfig()
@@ -430,9 +398,9 @@ class Package
return @mainModulePath if @resolvedMainModulePath
@resolvedMainModulePath = true
- if @bundledPackage and packagesCache[@name]?
- if packagesCache[@name].main
- @mainModulePath = "#{atom.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}"
+ if @bundledPackage and @packageManager.packagesCache[@name]?
+ if @packageManager.packagesCache[@name].main
+ @mainModulePath = "#{@packageManager.resourcePath}#{path.sep}#{@packageManager.packagesCache[@name].main}"
else
@mainModulePath = null
else
@@ -466,7 +434,7 @@ class Package
# Add dummy command so it appears in menu.
# The real command will be registered on package activation
try
- @activationCommandSubscriptions.add atom.commands.add selector, command, ->
+ @activationCommandSubscriptions.add @commandRegistry.add selector, command, ->
catch error
if error.code is 'EBADSELECTOR'
metadataPath = path.join(@path, 'package.json')
@@ -474,7 +442,7 @@ class Package
error.stack += "\n at #{metadataPath}:1:1"
throw error
- @activationCommandSubscriptions.add atom.commands.onWillDispatch (event) =>
+ @activationCommandSubscriptions.add @commandRegistry.onWillDispatch (event) =>
return unless event.type is command
currentTarget = event.target
while currentTarget
@@ -505,7 +473,7 @@ class Package
@activationHookSubscriptions = new CompositeDisposable
for hook in @getActivationHooks()
do (hook) =>
- @activationHookSubscriptions.add(atom.packages.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0
+ @activationHookSubscriptions.add(@packageManager.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0
return
@@ -567,7 +535,7 @@ class Package
isCompatible: ->
return @compatible if @compatible?
- if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0
+ if @path.indexOf(path.join(@packageManager.resourcePath, 'node_modules') + path.sep) is 0
# Bundled packages are always considered compatible
@compatible = true
else if @getMainModulePath()
@@ -603,7 +571,7 @@ class Package
stderr = ''
stdout = ''
new BufferedProcess({
- command: atom.packages.getApmPath()
+ command: @packageManager.getApmPath()
args: ['rebuild', '--no-color']
options: {cwd: @path}
stderr: (output) -> stderr += output
@@ -625,7 +593,7 @@ class Package
# This information is cached in local storage on a per package/version basis
# to minimize the impact on startup time.
getIncompatibleNativeModules: ->
- unless atom.inDevMode()
+ unless @devMode
try
if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey())
return JSON.parse(arrayAsString)
@@ -666,4 +634,4 @@ class Package
detail = error.message
stack = error.stack ? error
- atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
+ @notificationManager.addFatalError(message, {stack, detail, dismissable: true})
diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee
index 91f27858e..eaa26a9fe 100644
--- a/src/pane-axis-element.coffee
+++ b/src/pane-axis-element.coffee
@@ -8,7 +8,9 @@ class PaneAxisElement extends HTMLElement
detachedCallback: ->
@subscriptions.dispose()
- initialize: (@model) ->
+ initialize: (@model, {@views}) ->
+ throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
+
@subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
@subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
@subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
@@ -27,7 +29,7 @@ class PaneAxisElement extends HTMLElement
element?.nodeName.toLowerCase() is 'atom-pane-resize-handle'
childAdded: ({child, index}) ->
- view = atom.views.getView(child)
+ view = @views.getView(child)
@insertBefore(view, @children[index * 2])
prevElement = view.previousSibling
@@ -43,7 +45,7 @@ class PaneAxisElement extends HTMLElement
@insertBefore(resizeHandle, nextElement)
childRemoved: ({child}) ->
- view = atom.views.getView(child)
+ view = @views.getView(child)
siblingView = view.previousSibling
# make sure next sibling view is pane resize view
if siblingView? and @isPaneResizeHandleElement(siblingView)
diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee
index 57d07d8a9..d104e984b 100644
--- a/src/pane-axis.coffee
+++ b/src/pane-axis.coffee
@@ -4,19 +4,16 @@ Model = require './model'
module.exports =
class PaneAxis extends Model
- atom.deserializers.add(this)
-
parent: null
container: null
orientation: null
- @deserialize: (state, params) ->
- container = params?.container
- state.container = container
- state.children = state.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
+ @deserialize: (state, {deserializers}) ->
+ state.children = state.children.map (childState) ->
+ deserializers.deserialize(childState)
new this(state)
- constructor: ({@container, @orientation, children, flexScale}={}) ->
+ constructor: ({@orientation, children, flexScale}={}) ->
@emitter = new Emitter
@subscriptionsByChild = new WeakMap
@subscriptions = new CompositeDisposable
@@ -43,7 +40,10 @@ class PaneAxis extends Model
getContainer: -> @container
- setContainer: (@container) -> @container
+ setContainer: (container) ->
+ if container and container isnt @container
+ @container = container
+ child.setContainer(container) for child in @children
getOrientation: -> @orientation
diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee
index b3d4e0036..9da452558 100644
--- a/src/pane-container-element.coffee
+++ b/src/pane-container-element.coffee
@@ -7,7 +7,9 @@ class PaneContainerElement extends HTMLElement
@subscriptions = new CompositeDisposable
@classList.add 'panes'
- initialize: (@model) ->
+ initialize: (@model, {@views}) ->
+ throw new Error("Must pass a views parameter when initializing PaneContainerElements") unless @views?
+
@subscriptions.add @model.observeRoot(@rootChanged.bind(this))
this
@@ -15,7 +17,7 @@ class PaneContainerElement extends HTMLElement
focusedElement = document.activeElement if @hasFocus()
@firstChild?.remove()
if root?
- view = atom.views.getView(root)
+ view = @views.getView(root)
@appendChild(view)
focusedElement?.focus()
@@ -40,7 +42,7 @@ class PaneContainerElement extends HTMLElement
y = pointB.y - pointA.y
Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
- paneView = atom.views.getView(@model.getActivePane())
+ paneView = @views.getView(@model.getActivePane())
box = @boundingBoxForPaneView(paneView)
paneViews = _.toArray(@querySelectorAll('atom-pane'))
diff --git a/src/pane-container.coffee b/src/pane-container.coffee
index 786c4b84f..8ad082a5f 100644
--- a/src/pane-container.coffee
+++ b/src/pane-container.coffee
@@ -7,44 +7,37 @@ ItemRegistry = require './item-registry'
module.exports =
class PaneContainer extends Model
- atom.deserializers.add(this)
-
- @version: 1
-
+ serializationVersion: 1
root: null
-
- @deserialize: (state) ->
- container = Object.create(@prototype) # allows us to pass a self reference to our child before invoking constructor
- state.root = atom.deserializers.deserialize(state.root, {container})
- state.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes')
- state.activePane = find state.root.getPanes(), (pane) -> pane.id is state.activePaneId
- @call(container, state) # run constructor
- container
+ stoppedChangingActivePaneItemDelay: 100
+ stoppedChangingActivePaneItemTimeout: null
constructor: (params) ->
super
- @activePane = params?.activePane
-
+ {@config, applicationDelegate, notificationManager, deserializerManager} = params
@emitter = new Emitter
@subscriptions = new CompositeDisposable
-
@itemRegistry = new ItemRegistry
- @setRoot(params?.root ? new Pane)
- @setActivePane(@getPanes()[0]) unless @getActivePane()
-
- @destroyEmptyPanes() if params?.destroyEmptyPanes
-
+ @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager}))
+ @setActivePane(@getRoot())
@monitorActivePaneItem()
@monitorPaneItems()
serialize: (params) ->
deserializer: 'PaneContainer'
- version: @constructor.version
+ version: @serializationVersion
root: @root?.serialize()
activePaneId: @activePane.id
+ deserialize: (state, deserializerManager) ->
+ return unless state.version is @serializationVersion
+ @setRoot(deserializerManager.deserialize(state.root))
+ activePane = find @getRoot().getPanes(), (pane) -> pane.id is state.activePaneId
+ @setActivePane(activePane ? @getPanes()[0])
+ @destroyEmptyPanes() if @config.get('core.destroyEmptyPanes')
+
onDidChangeRoot: (fn) ->
@emitter.on 'did-change-root', fn
@@ -82,6 +75,9 @@ class PaneContainer extends Model
onDidChangeActivePaneItem: (fn) ->
@emitter.on 'did-change-active-pane-item', fn
+ onDidStopChangingActivePaneItem: (fn) ->
+ @emitter.on 'did-stop-changing-active-pane-item', fn
+
observeActivePaneItem: (fn) ->
fn(@getActivePaneItem())
@onDidChangeActivePaneItem(fn)
@@ -189,12 +185,18 @@ class PaneContainer extends Model
# Called by Model superclass when destroyed
destroyed: ->
+ @cancelStoppedChangingActivePaneItemTimeout()
pane.destroy() for pane in @getPanes()
@subscriptions.dispose()
@emitter.dispose()
+ cancelStoppedChangingActivePaneItemTimeout: ->
+ if @stoppedChangingActivePaneItemTimeout?
+ clearTimeout(@stoppedChangingActivePaneItemTimeout)
+
monitorActivePaneItem: ->
childSubscription = null
+
@subscriptions.add @observeActivePane (activePane) =>
if childSubscription?
@subscriptions.remove(childSubscription)
@@ -202,6 +204,14 @@ class PaneContainer extends Model
childSubscription = activePane.observeActiveItem (activeItem) =>
@emitter.emit 'did-change-active-pane-item', activeItem
+ @cancelStoppedChangingActivePaneItemTimeout()
+ stoppedChangingActivePaneItemCallback = =>
+ @stoppedChangingActivePaneItemTimeout = null
+ @emitter.emit 'did-stop-changing-active-pane-item', activeItem
+ @stoppedChangingActivePaneItemTimeout =
+ setTimeout(
+ stoppedChangingActivePaneItemCallback,
+ @stoppedChangingActivePaneItemDelay)
@subscriptions.add(childSubscription)
diff --git a/src/pane-element.coffee b/src/pane-element.coffee
index c6ded79c4..a0113d127 100644
--- a/src/pane-element.coffee
+++ b/src/pane-element.coffee
@@ -44,14 +44,17 @@ class PaneElement extends HTMLElement
event.stopPropagation()
@getModel().activate()
pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path
- atom.open({pathsToOpen}) if pathsToOpen.length > 0
+ @applicationDelegate.open({pathsToOpen}) if pathsToOpen.length > 0
@addEventListener 'focus', handleFocus, true
@addEventListener 'blur', handleBlur, true
@addEventListener 'dragover', handleDragOver
@addEventListener 'drop', handleDrop
- initialize: (@model) ->
+ initialize: (@model, {@views, @applicationDelegate}) ->
+ throw new Error("Must pass a views parameter when initializing PaneElements") unless @views?
+ throw new Error("Must pass an applicationDelegate parameter when initializing PaneElements") unless @applicationDelegate?
+
@subscriptions.add @model.onDidActivate(@activated.bind(this))
@subscriptions.add @model.observeActive(@activeStatusChanged.bind(this))
@subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
@@ -78,7 +81,7 @@ class PaneElement extends HTMLElement
return unless item?
hasFocus = @hasFocus()
- itemView = atom.views.getView(item)
+ itemView = @views.getView(item)
if itemPath = item.getPath?()
@dataset.activeItemName = path.basename(itemPath)
@@ -109,7 +112,7 @@ class PaneElement extends HTMLElement
itemView.style.display = 'none'
itemRemoved: ({item, index, destroyed}) ->
- if viewToRemove = atom.views.getView(item)
+ if viewToRemove = @views.getView(item)
viewToRemove.remove()
paneDestroyed: ->
@@ -118,35 +121,9 @@ class PaneElement extends HTMLElement
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale
- getActiveView: -> atom.views.getView(@model.getActiveItem())
+ getActiveView: -> @views.getView(@model.getActiveItem())
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
-atom.commands.add 'atom-workspace',
- 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
- 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
- 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)
- 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1)
- 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2)
- 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3)
- 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4)
- 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5)
- 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6)
- 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7)
- 'pane:show-item-9': -> @getModel().getActivePane().activateItemAtIndex(8)
- 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight()
- 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft()
-
-atom.commands.add 'atom-pane',
- 'pane:save-items': -> @getModel().saveItems()
- 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true)
- 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true)
- 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true)
- 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true)
- 'pane:close': -> @getModel().close()
- 'pane:close-other-items': -> @getModel().destroyInactiveItems()
- 'pane:increase-size': -> @getModel().increaseSize()
- 'pane:decrease-size': -> @getModel().decreaseSize()
-
module.exports = PaneElement = document.registerElement 'atom-pane', prototype: PaneElement.prototype
diff --git a/src/pane.coffee b/src/pane.coffee
index a146b66fc..92be02575 100644
--- a/src/pane.coffee
+++ b/src/pane.coffee
@@ -10,30 +10,31 @@ TextEditor = require './text-editor'
# the default configuration, tabs are also displayed for each item.
module.exports =
class Pane extends Model
- atom.deserializers.add(this)
-
container: undefined
activeItem: undefined
focused: false
- @deserialize: (state, params) ->
+ @deserialize: (state, {deserializers, applicationDelegate, config, notifications}) ->
{items, activeItemURI, activeItemUri} = state
- state.container = params?.container
activeItemURI ?= activeItemUri
- state.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState))
+ state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))
state.activeItem = find state.items, (item) ->
if typeof item.getURI is 'function'
itemURI = item.getURI()
itemURI is activeItemURI
-
- new this(state)
+ new Pane(extend(state, {
+ deserializerManager: deserializers,
+ notificationManager: notifications,
+ config, applicationDelegate
+ }))
constructor: (params) ->
super
- @container = params?.container
- @activeItem = params?.activeItem
- @focused = params?.focused
+ {
+ @activeItem, @focused, @applicationDelegate, @notificationManager, @config,
+ @deserializerManager
+ } = params
@emitter = new Emitter
@itemSubscriptions = new WeakMap
@@ -61,7 +62,7 @@ class Pane extends Model
getContainer: -> @container
setContainer: (container) ->
- unless container is @container
+ if container and container isnt @container
@container = container
container.didAddPane({pane: this})
@@ -395,7 +396,7 @@ class Pane extends Model
@items.splice(index, 1)
@emitter.emit 'did-remove-item', {item, index, destroyed: not moved, moved}
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
- @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
+ @destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
# Public: Move the given item to the given index.
#
@@ -461,7 +462,7 @@ class Pane extends Model
else
return true
- chosen = atom.confirm
+ chosen = @applicationDelegate.confirm
message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?"
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: ["Save", "Cancel", "Don't Save"]
@@ -514,7 +515,7 @@ class Pane extends Model
saveOptions = item.getSaveDialogOptions?() ? {}
saveOptions.defaultPath ?= item.getPath()
- newItemPath = atom.showSaveDialogSync(saveOptions)
+ newItemPath = @applicationDelegate.showSaveDialog(saveOptions)
if newItemPath
try
item.saveAs(newItemPath)
@@ -524,7 +525,8 @@ class Pane extends Model
# Public: Save all items.
saveItems: ->
- @saveItem(item) for item in @getItems()
+ for item in @getItems()
+ @saveItem(item) if item.isModified?()
return
# Public: Return the first item that matches the given URI or undefined if
@@ -554,7 +556,7 @@ class Pane extends Model
copyActiveItem: ->
if @activeItem?
- @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
+ @activeItem.copy?() ? @deserializerManager.deserialize(@activeItem.serialize())
###
Section: Lifecycle
@@ -646,7 +648,7 @@ class Pane extends Model
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
@setFlexScale(1)
- newPane = new @constructor(params)
+ newPane = new Pane(extend({@applicationDelegate, @deserializerManager, @config}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
@@ -678,6 +680,30 @@ class Pane extends Model
else
@splitRight()
+ # If the parent is a vertical axis, returns its first child if it is a pane;
+ # otherwise returns this pane.
+ findTopmostSibling: ->
+ if @parent.orientation is 'vertical'
+ [topmostSibling] = @parent.children
+ if topmostSibling instanceof PaneAxis
+ this
+ else
+ topmostSibling
+ else
+ this
+
+ # If the parent is a vertical axis, returns its last child if it is a pane;
+ # otherwise returns a new pane created by splitting this pane bottomward.
+ findOrCreateBottommostSibling: ->
+ if @parent.orientation is 'vertical'
+ bottommostSibling = last(@parent.children)
+ if bottommostSibling instanceof PaneAxis
+ @splitRight()
+ else
+ bottommostSibling
+ else
+ @splitDown()
+
close: ->
@destroy() if @confirmClose()
@@ -688,12 +714,12 @@ class Pane extends Model
handleSaveError: (error, item) ->
itemPath = error.path ? item?.getPath?()
- addWarningWithPath = (message, options) ->
+ addWarningWithPath = (message, options) =>
message = "#{message} '#{itemPath}'" if itemPath
- atom.notifications.addWarning(message, options)
+ @notificationManager.addWarning(message, options)
if error.code is 'EISDIR' or error.message?.endsWith?('is a directory')
- atom.notifications.addWarning("Unable to save file: #{error.message}")
+ @notificationManager.addWarning("Unable to save file: #{error.message}")
else if error.code is 'EACCES'
addWarningWithPath('Unable to save file: Permission denied')
else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST']
@@ -716,6 +742,6 @@ class Pane extends Model
addWarningWithPath('Unable to save file: Invalid seek')
else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message)
fileName = errorMatch[1]
- atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
+ @notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
else
throw error
diff --git a/src/panel-container-element.coffee b/src/panel-container-element.coffee
index e1459e6d6..b66a0cd90 100644
--- a/src/panel-container-element.coffee
+++ b/src/panel-container-element.coffee
@@ -4,9 +4,10 @@ class PanelContainerElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
- initialize: (@model) ->
+ initialize: (@model, {@views}) ->
+ throw new Error("Must pass a views parameter when initializing PanelContainerElements") unless @views?
+
@subscriptions.add @model.onDidAddPanel(@panelAdded.bind(this))
- @subscriptions.add @model.onDidRemovePanel(@panelRemoved.bind(this))
@subscriptions.add @model.onDidDestroy(@destroyed.bind(this))
@classList.add(@model.getLocation())
this
@@ -14,7 +15,7 @@ class PanelContainerElement extends HTMLElement
getModel: -> @model
panelAdded: ({panel, index}) ->
- panelElement = atom.views.getView(panel)
+ panelElement = @views.getView(panel)
panelElement.classList.add(@model.getLocation())
if @model.isModal()
panelElement.classList.add("overlay", "from-top")
@@ -32,9 +33,6 @@ class PanelContainerElement extends HTMLElement
@subscriptions.add panel.onDidChangeVisible (visible) =>
@hideAllPanelsExcept(panel) if visible
- panelRemoved: ({panel, index}) ->
- @removeChild(atom.views.getView(panel))
-
destroyed: ->
@subscriptions.dispose()
@parentNode?.removeChild(this)
diff --git a/src/panel-element.coffee b/src/panel-element.coffee
index 287d34940..72fd6ebf2 100644
--- a/src/panel-element.coffee
+++ b/src/panel-element.coffee
@@ -5,7 +5,9 @@ class PanelElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
- initialize: (@model) ->
+ initialize: (@model, {@views}) ->
+ throw new Error("Must pass a views parameter when initializing PanelElements") unless @views?
+
@appendChild(@getItemView())
@classList.add(@model.getClassName().split(' ')...) if @model.getClassName()?
@@ -17,7 +19,7 @@ class PanelElement extends HTMLElement
@model ?= new Panel
getItemView: ->
- atom.views.getView(@getModel().getItem())
+ @views.getView(@getModel().getItem())
attachedCallback: ->
@visibleChanged(@getModel().isVisible())
@@ -30,6 +32,6 @@ class PanelElement extends HTMLElement
destroyed: ->
@subscriptions.dispose()
- @parentNode?.removeChild(this)
+ @remove()
module.exports = PanelElement = document.registerElement 'atom-panel', prototype: PanelElement.prototype
diff --git a/src/project.coffee b/src/project.coffee
index b3f05a942..935e3a213 100644
--- a/src/project.coffee
+++ b/src/project.coffee
@@ -3,7 +3,7 @@ url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
-{Emitter} = require 'event-kit'
+{Emitter, Disposable} = require 'event-kit'
TextBuffer = require 'text-buffer'
DefaultDirectoryProvider = require './default-directory-provider'
@@ -17,68 +17,35 @@ GitRepositoryProvider = require './git-repository-provider'
# An instance of this class is always available as the `atom.project` global.
module.exports =
class Project extends Model
- atom.deserializers.add(this)
-
###
Section: Construction and Destruction
###
- @deserialize: (state) ->
- state.buffers = _.compact state.buffers.map (bufferState) ->
- # Check that buffer's file path is accessible
- return if fs.isDirectorySync(bufferState.filePath)
- if bufferState.filePath
- try
- fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
- catch error
- return unless error.code is 'ENOENT'
-
- atom.deserializers.deserialize(bufferState)
-
- new this(state)
-
- constructor: ({path, paths, @buffers}={}) ->
+ constructor: ({@notificationManager, packageManager, config}) ->
@emitter = new Emitter
- @buffers ?= []
+ @buffers = []
+ @paths = []
@rootDirectories = []
@repositories = []
-
@directoryProviders = []
@defaultDirectoryProvider = new DefaultDirectoryProvider()
- atom.packages.serviceHub.consume(
- 'atom.directory-provider',
- '^0.1.0',
- (provider) => @directoryProviders.unshift(provider))
-
- # Mapping from the real path of a {Directory} to a {Promise} that resolves
- # to either a {Repository} or null. Ideally, the {Directory} would be used
- # as the key; however, there can be multiple {Directory} objects created for
- # the same real path, so it is not a good key.
@repositoryPromisesByPath = new Map()
-
- @repositoryProviders = [new GitRepositoryProvider(this)]
- atom.packages.serviceHub.consume(
- 'atom.repository-provider',
- '^0.1.0',
- (provider) =>
- @repositoryProviders.push(provider)
-
- # If a path in getPaths() does not have a corresponding Repository, try
- # to assign one by running through setPaths() again now that
- # @repositoryProviders has been updated.
- if null in @repositories
- @setPaths(@getPaths())
- )
-
- @subscribeToBuffer(buffer) for buffer in @buffers
-
- paths ?= _.compact([path])
- @setPaths(paths)
+ @repositoryProviders = [new GitRepositoryProvider(this, config)]
+ @consumeServices(packageManager)
destroyed: ->
- buffer.destroy() for buffer in @getBuffers()
+ buffer.destroy() for buffer in @buffers
@setPaths([])
+ reset: (packageManager) ->
+ @emitter.dispose()
+ @emitter = new Emitter
+
+ buffer?.destroy() for buffer in @buffers
+ @buffers = []
+ @setPaths([])
+ @consumeServices(packageManager)
+
destroyUnretainedBuffers: ->
buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
return
@@ -87,6 +54,22 @@ class Project extends Model
Section: Serialization
###
+ deserialize: (state, deserializerManager) ->
+ states.paths = [state.path] if state.path? # backward compatibility
+
+ @buffers = _.compact state.buffers.map (bufferState) ->
+ # Check that buffer's file path is accessible
+ return if fs.isDirectorySync(bufferState.filePath)
+ if bufferState.filePath
+ try
+ fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
+ catch error
+ return unless error.code is 'ENOENT'
+ deserializerManager.deserialize(bufferState)
+
+ @subscribeToBuffer(buffer) for buffer in @buffers
+ @setPaths(state.paths)
+
serialize: ->
deserializer: 'Project'
paths: @getPaths()
@@ -291,39 +274,25 @@ class Project extends Model
Section: Private
###
- # Given a path to a file, this constructs and associates a new
- # {TextEditor}, showing the file.
- #
- # * `filePath` The {String} path of the file to associate with.
- # * `options` Options that you can pass to the {TextEditor} constructor.
- #
- # Returns a promise that resolves to an {TextEditor}.
- open: (filePath, options={}) ->
- filePath = @resolvePath(filePath)
+ consumeServices: ({serviceHub}) ->
+ serviceHub.consume(
+ 'atom.directory-provider',
+ '^0.1.0',
+ (provider) =>
+ @directoryProviders.unshift(provider)
+ new Disposable =>
+ @directoryProviders.splice(@directoryProviders.indexOf(provider), 1)
+ )
- if filePath?
- try
- fs.closeSync(fs.openSync(filePath, 'r'))
- catch error
- # allow ENOENT errors to create an editor for paths that dont exist
- throw error unless error.code is 'ENOENT'
-
- absoluteFilePath = @resolvePath(filePath)
-
- fileSize = fs.getSizeSync(absoluteFilePath)
-
- if fileSize >= 20 * 1048576 # 20MB
- choice = atom.confirm
- message: 'Atom will be unresponsive during the loading of very large files.'
- detailedMessage: "Do you still want to load this file?"
- buttons: ["Proceed", "Cancel"]
- if choice is 1
- error = new Error
- error.code = 'CANCELLED'
- throw error
-
- @bufferForPath(absoluteFilePath).then (buffer) =>
- @buildEditorForBuffer(buffer, _.extend({fileSize}, options))
+ serviceHub.consume(
+ 'atom.repository-provider',
+ '^0.1.0',
+ (provider) =>
+ @repositoryProviders.push(provider)
+ @setPaths(@getPaths()) if null in @repositories
+ new Disposable =>
+ @repositoryProviders.splice(@repositoryProviders.indexOf(provider), 1)
+ )
# Retrieves all the {TextBuffer}s in the project; that is, the
# buffers for all open files.
@@ -404,11 +373,6 @@ class Project extends Model
[buffer] = @buffers.splice(index, 1)
buffer?.destroy()
- buildEditorForBuffer: (buffer, editorOptions) ->
- largeFileMode = editorOptions.fileSize >= 2 * 1048576 # 2MB
- editor = new TextEditor(_.extend({buffer, largeFileMode, registerEditor: true}, editorOptions))
- editor
-
eachBuffer: (args...) ->
subscriber = args.shift() if args.length > 1
callback = args.shift()
@@ -421,9 +385,9 @@ class Project extends Model
subscribeToBuffer: (buffer) ->
buffer.onDidDestroy => @removeBuffer(buffer)
- buffer.onWillThrowWatchError ({error, handle}) ->
+ buffer.onWillThrowWatchError ({error, handle}) =>
handle()
- atom.notifications.addWarning """
+ @notificationManager.addWarning """
Unable to read file after file `#{error.eventType}` event.
Make sure you have permission to access `#{buffer.getPath()}`.
""",
diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee
new file mode 100644
index 000000000..80b7ff6c6
--- /dev/null
+++ b/src/register-default-commands.coffee
@@ -0,0 +1,212 @@
+ipc = require 'ipc'
+
+module.exports = ({commandRegistry, commandInstaller, config}) ->
+ commandRegistry.add 'atom-workspace',
+ 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
+ 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
+ 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)
+ 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1)
+ 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2)
+ 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3)
+ 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4)
+ 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5)
+ 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6)
+ 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7)
+ 'pane:show-item-9': -> @getModel().getActivePane().activateItemAtIndex(8)
+ 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight()
+ 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft()
+ 'window:increase-font-size': -> @getModel().increaseFontSize()
+ 'window:decrease-font-size': -> @getModel().decreaseFontSize()
+ 'window:reset-font-size': -> @getModel().resetFontSize()
+ 'application:about': -> ipc.send('command', 'application:about')
+ 'application:show-preferences': -> ipc.send('command', 'application:show-settings')
+ 'application:show-settings': -> ipc.send('command', 'application:show-settings')
+ 'application:quit': -> ipc.send('command', 'application:quit')
+ 'application:hide': -> ipc.send('command', 'application:hide')
+ 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
+ 'application:install-update': -> ipc.send('command', 'application:install-update')
+ 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
+ 'application:new-window': -> ipc.send('command', 'application:new-window')
+ 'application:new-file': -> ipc.send('command', 'application:new-file')
+ 'application:open': -> ipc.send('command', 'application:open')
+ 'application:open-file': -> ipc.send('command', 'application:open-file')
+ 'application:open-folder': -> ipc.send('command', 'application:open-folder')
+ 'application:open-dev': -> ipc.send('command', 'application:open-dev')
+ 'application:open-safe': -> ipc.send('command', 'application:open-safe')
+ 'application:add-project-folder': -> atom.addProjectFolder()
+ 'application:minimize': -> ipc.send('command', 'application:minimize')
+ 'application:zoom': -> ipc.send('command', 'application:zoom')
+ 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
+ 'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
+ 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
+ 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
+ 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
+ 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
+ 'application:open-license': -> @getModel().openLicense()
+ 'window:run-package-specs': -> @runPackageSpecs()
+ 'window:focus-next-pane': -> @getModel().activateNextPane()
+ 'window:focus-previous-pane': -> @getModel().activatePreviousPane()
+ 'window:focus-pane-above': -> @focusPaneViewAbove()
+ 'window:focus-pane-below': -> @focusPaneViewBelow()
+ 'window:focus-pane-on-left': -> @focusPaneViewOnLeft()
+ 'window:focus-pane-on-right': -> @focusPaneViewOnRight()
+ 'window:save-all': -> @getModel().saveAll()
+ 'window:toggle-invisibles': -> config.set("editor.showInvisibles", not config.get("editor.showInvisibles"))
+ 'window:log-deprecation-warnings': -> Grim.logDeprecations()
+ 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent"))
+ 'pane:reopen-closed-item': -> @getModel().reopenItem()
+ 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane()
+ 'core:save': -> @getModel().saveActivePaneItem()
+ 'core:save-as': -> @getModel().saveActivePaneItemAs()
+
+ if process.platform is 'darwin'
+ commandRegistry.add 'atom-workspace', 'window:install-shell-commands', ->
+ commandInstaller.installShellCommandsInteractively()
+
+ commandRegistry.add 'atom-pane',
+ 'pane:save-items': -> @getModel().saveItems()
+ 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true)
+ 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true)
+ 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true)
+ 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true)
+ 'pane:close': -> @getModel().close()
+ 'pane:close-other-items': -> @getModel().destroyInactiveItems()
+ 'pane:increase-size': -> @getModel().increaseSize()
+ 'pane:decrease-size': -> @getModel().decreaseSize()
+
+ commandRegistry.add 'atom-text-editor', stopEventPropagation(
+ 'core:undo': -> @undo()
+ 'core:redo': -> @redo()
+ 'core:move-left': -> @moveLeft()
+ 'core:move-right': -> @moveRight()
+ 'core:select-left': -> @selectLeft()
+ 'core:select-right': -> @selectRight()
+ 'core:select-up': -> @selectUp()
+ 'core:select-down': -> @selectDown()
+ 'core:select-all': -> @selectAll()
+ 'editor:select-word': -> @selectWordsContainingCursors()
+ 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections()
+ 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph()
+ 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph()
+ 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine()
+ 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine()
+ 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine()
+ 'editor:move-to-end-of-line': -> @moveToEndOfLine()
+ 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine()
+ 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord()
+ 'editor:move-to-end-of-word': -> @moveToEndOfWord()
+ 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord()
+ 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary()
+ 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary()
+ 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary()
+ 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary()
+ 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph()
+ 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph()
+ 'editor:select-to-end-of-line': -> @selectToEndOfLine()
+ 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine()
+ 'editor:select-to-end-of-word': -> @selectToEndOfWord()
+ 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord()
+ 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord()
+ 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary()
+ 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary()
+ 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary()
+ 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary()
+ 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine()
+ 'editor:select-line': -> @selectLinesContainingCursors()
+ )
+
+ commandRegistry.add 'atom-text-editor', stopEventPropagationAndGroupUndo(config,
+ 'core:backspace': -> @backspace()
+ 'core:delete': -> @delete()
+ 'core:cut': -> @cutSelectedText()
+ 'core:copy': -> @copySelectedText()
+ 'core:paste': -> @pasteText()
+ 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
+ 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
+ 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
+ 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine()
+ 'editor:delete-to-end-of-line': -> @deleteToEndOfLine()
+ 'editor:delete-to-end-of-word': -> @deleteToEndOfWord()
+ 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword()
+ 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword()
+ 'editor:delete-line': -> @deleteLine()
+ 'editor:cut-to-end-of-line': -> @cutToEndOfLine()
+ 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine()
+ 'editor:transpose': -> @transpose()
+ 'editor:upper-case': -> @upperCase()
+ 'editor:lower-case': -> @lowerCase()
+ 'editor:copy-selection': -> @copyOnlySelectedText()
+ )
+
+ commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagation(
+ 'core:move-up': -> @moveUp()
+ 'core:move-down': -> @moveDown()
+ 'core:move-to-top': -> @moveToTop()
+ 'core:move-to-bottom': -> @moveToBottom()
+ 'core:page-up': -> @pageUp()
+ 'core:page-down': -> @pageDown()
+ 'core:select-to-top': -> @selectToTop()
+ 'core:select-to-bottom': -> @selectToBottom()
+ 'core:select-page-up': -> @selectPageUp()
+ 'core:select-page-down': -> @selectPageDown()
+ 'editor:add-selection-below': -> @addSelectionBelow()
+ 'editor:add-selection-above': -> @addSelectionAbove()
+ 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines()
+ 'editor:toggle-soft-tabs': -> @toggleSoftTabs()
+ 'editor:toggle-soft-wrap': -> @toggleSoftWrapped()
+ 'editor:fold-all': -> @foldAll()
+ 'editor:unfold-all': -> @unfoldAll()
+ 'editor:fold-current-row': -> @foldCurrentRow()
+ 'editor:unfold-current-row': -> @unfoldCurrentRow()
+ 'editor:fold-selection': -> @foldSelectedLines()
+ 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0)
+ 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1)
+ 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2)
+ 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3)
+ 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4)
+ 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5)
+ 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
+ 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
+ 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
+ 'editor:log-cursor-scope': -> @logCursorScope()
+ 'editor:copy-path': -> @copyPathToClipboard()
+ 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide'))
+ 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers'))
+ 'editor:scroll-to-cursor': -> @scrollToCursorPosition()
+ )
+
+ commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(config,
+ 'editor:indent': -> @indent()
+ 'editor:auto-indent': -> @autoIndentSelectedRows()
+ 'editor:indent-selected-rows': -> @indentSelectedRows()
+ 'editor:outdent-selected-rows': -> @outdentSelectedRows()
+ 'editor:newline': -> @insertNewline()
+ 'editor:newline-below': -> @insertNewlineBelow()
+ 'editor:newline-above': -> @insertNewlineAbove()
+ 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection()
+ 'editor:checkout-head-revision': -> @checkoutHeadRevision()
+ 'editor:move-line-up': -> @moveLineUp()
+ 'editor:move-line-down': -> @moveLineDown()
+ 'editor:duplicate-lines': -> @duplicateLines()
+ 'editor:join-lines': -> @joinLines()
+ )
+
+stopEventPropagation = (commandListeners) ->
+ newCommandListeners = {}
+ for commandName, commandListener of commandListeners
+ do (commandListener) ->
+ newCommandListeners[commandName] = (event) ->
+ event.stopPropagation()
+ commandListener.call(@getModel(), event)
+ newCommandListeners
+
+stopEventPropagationAndGroupUndo = (config, commandListeners) ->
+ newCommandListeners = {}
+ for commandName, commandListener of commandListeners
+ do (commandListener) ->
+ newCommandListeners[commandName] = (event) ->
+ event.stopPropagation()
+ model = @getModel()
+ model.transact config.get('editor.undoGroupingInterval'), ->
+ commandListener.call(model, event)
+ newCommandListeners
diff --git a/src/scoped-properties.coffee b/src/scoped-properties.coffee
index 8bf4c5ed3..c7257083e 100644
--- a/src/scoped-properties.coffee
+++ b/src/scoped-properties.coffee
@@ -3,21 +3,21 @@ CSON = require 'season'
module.exports =
class ScopedProperties
- @load: (scopedPropertiesPath, callback) ->
+ @load: (scopedPropertiesPath, config, callback) ->
CSON.readFile scopedPropertiesPath, (error, scopedProperties={}) ->
if error?
callback(error)
else
- callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties))
+ callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties, config))
- constructor: (@path, @scopedProperties) ->
+ constructor: (@path, @scopedProperties, @config) ->
activate: ->
for selector, properties of @scopedProperties
- atom.config.set(null, properties, scopeSelector: selector, source: @path)
+ @config.set(null, properties, scopeSelector: selector, source: @path)
return
deactivate: ->
for selector of @scopedProperties
- atom.config.unset(null, scopeSelector: selector, source: @path)
+ @config.unset(null, scopeSelector: selector, source: @path)
return
diff --git a/src/selection.coffee b/src/selection.coffee
index 80929cac7..2ba66ebb0 100644
--- a/src/selection.coffee
+++ b/src/selection.coffee
@@ -14,7 +14,7 @@ class Selection extends Model
initialScreenRange: null
wordwise: false
- constructor: ({@cursor, @marker, @editor, id}) ->
+ constructor: ({@cursor, @marker, @editor, id, @clipboard}) ->
@emitter = new Emitter
@assignId(id)
@@ -611,7 +611,7 @@ class Selection extends Model
startLevel = @editor.indentLevelForLine(precedingText)
if maintainClipboard
- {text: clipboardText, metadata} = atom.clipboard.readWithMetadata()
+ {text: clipboardText, metadata} = @clipboard.readWithMetadata()
metadata ?= {}
unless metadata.selections?
metadata.selections = [{
@@ -624,9 +624,9 @@ class Selection extends Model
indentBasis: startLevel,
fullLine: fullLine
})
- atom.clipboard.write([clipboardText, selectionText].join("\n"), metadata)
+ @clipboard.write([clipboardText, selectionText].join("\n"), metadata)
else
- atom.clipboard.write(selectionText, {
+ @clipboard.write(selectionText, {
indentBasis: startLevel,
fullLine: fullLine
})
diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee
index bf969dee2..da8af3f2e 100644
--- a/src/storage-folder.coffee
+++ b/src/storage-folder.coffee
@@ -4,12 +4,16 @@ fs = require "fs-plus"
module.exports =
class StorageFolder
constructor: (containingPath) ->
- @path = path.join(containingPath, "storage")
+ @path = path.join(containingPath, "storage") if containingPath?
store: (name, object) ->
+ return unless @path?
+
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
load: (name) ->
+ return unless @path?
+
statePath = @pathForKey(name)
try
stateString = fs.readFileSync(statePath, 'utf8')
diff --git a/src/style-manager.coffee b/src/style-manager.coffee
index cfe86b3fe..8f932d229 100644
--- a/src/style-manager.coffee
+++ b/src/style-manager.coffee
@@ -1,6 +1,7 @@
fs = require 'fs-plus'
path = require 'path'
{Emitter, Disposable} = require 'event-kit'
+StylesElement = require './styles-element'
# Extended: A singleton instance of this class available via `atom.styles`,
# which you can use to globally query and observe the set of active style
@@ -9,7 +10,7 @@ path = require 'path'
# which clone and attach style elements in different contexts.
module.exports =
class StyleManager
- constructor: ->
+ constructor: ({@configDirPath}) ->
@emitter = new Emitter
@styleElements = []
@styleElementsBySourcePath = {}
@@ -154,6 +155,11 @@ class StyleManager
return
+ buildStylesElement: ->
+ stylesElement = new StylesElement
+ stylesElement.initialize(this)
+ stylesElement
+
###
Section: Paths
###
@@ -162,8 +168,10 @@ class StyleManager
#
# Returns a {String}.
getUserStyleSheetPath: ->
- stylesheetPath = fs.resolve(path.join(atom.getConfigDirPath(), 'styles'), ['css', 'less'])
+ return "" unless @configDirPath?
+
+ stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less'])
if fs.isFileSync(stylesheetPath)
stylesheetPath
else
- path.join(atom.getConfigDirPath(), 'styles.less')
+ path.join(@configDirPath, 'styles.less')
diff --git a/src/styles-element.coffee b/src/styles-element.coffee
index fc3b888cf..d1e6bf3d9 100644
--- a/src/styles-element.coffee
+++ b/src/styles-element.coffee
@@ -14,6 +14,7 @@ class StylesElement extends HTMLElement
@emitter.on 'did-update-style-element', callback
createdCallback: ->
+ @subscriptions = new CompositeDisposable
@emitter = new Emitter
@styleElementClonesByOriginalElement = new WeakMap
@@ -21,31 +22,29 @@ class StylesElement extends HTMLElement
if @context is 'atom-text-editor'
for styleElement in @children
@upgradeDeprecatedSelectors(styleElement)
- @initialize()
+
+ @context = @getAttribute('context') ? undefined
detachedCallback: ->
@subscriptions.dispose()
- @subscriptions = null
+ @subscriptions = new CompositeDisposable
attributeChangedCallback: (attrName, oldVal, newVal) ->
@contextChanged() if attrName is 'context'
- initialize: ->
- return if @subscriptions?
+ initialize: (@styleManager) ->
+ throw new Error("Must pass a styleManager parameter when initializing a StylesElement") unless @styleManager?
- @subscriptions = new CompositeDisposable
- @context = @getAttribute('context') ? undefined
-
- @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this))
- @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this))
- @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this))
+ @subscriptions.add @styleManager.observeStyleElements(@styleElementAdded.bind(this))
+ @subscriptions.add @styleManager.onDidRemoveStyleElement(@styleElementRemoved.bind(this))
+ @subscriptions.add @styleManager.onDidUpdateStyleElement(@styleElementUpdated.bind(this))
contextChanged: ->
return unless @subscriptions?
@styleElementRemoved(child) for child in Array::slice.call(@children)
@context = @getAttribute('context')
- @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements()
+ @styleElementAdded(styleElement) for styleElement in @styleManager.getStyleElements()
return
styleElementAdded: (styleElement) ->
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index 3dd9f317c..d72c50382 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -38,15 +38,15 @@ class TextEditorComponent
Object.defineProperty @prototype, "domNode",
get: -> @domNodeValue
set: (domNode) ->
- atom.assert domNode?, "TextEditorComponent::domNode was set to null."
+ @assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
- constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) ->
+ constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@observeConfig()
- @setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
+ @setScrollSensitivity(@config.get('editor.scrollSensitivity'))
@presenter = new TextEditorPresenter
model: @editor
@@ -58,6 +58,7 @@ class TextEditorComponent
cursorBlinkPeriod: @cursorBlinkPeriod
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
stoppedScrollingDelay: 200
+ config: @config
@presenter.onDidUpdateState(@requestUpdate)
@@ -70,10 +71,10 @@ class TextEditorComponent
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@domNode.appendChild(insertionPoint)
- @overlayManager = new OverlayManager(@presenter, @hostElement)
+ @overlayManager = new OverlayManager(@presenter, @hostElement, @views)
else
@domNode.classList.add('editor-contents')
- @overlayManager = new OverlayManager(@presenter, @domNode)
+ @overlayManager = new OverlayManager(@presenter, @domNode, @views)
@scrollViewNode = document.createElement('div')
@scrollViewNode.classList.add('scroll-view')
@@ -82,10 +83,10 @@ class TextEditorComponent
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
- @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool})
+ @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
- @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent)
+ @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, @grammars)
@presenter.setLinesYardstick(@linesYardstick)
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@@ -103,11 +104,11 @@ class TextEditorComponent
@disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged
@disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged
@disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
- unless atom.themes.isInitialLoadComplete()
- @disposables.add atom.themes.onDidChangeActiveThemes @onAllThemesLoaded
+ unless @themes.isInitialLoadComplete()
+ @disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded
@disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
- @disposables.add atom.views.pollDocument(@pollDOM)
+ @disposables.add @views.pollDocument(@pollDOM)
@updateSync()
@checkForVisibilityChange()
@@ -177,7 +178,7 @@ class TextEditorComponent
@overlayManager?.measureOverlays()
mountGutterContainerComponent: ->
- @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool})
+ @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
becameVisible: ->
@@ -204,10 +205,10 @@ class TextEditorComponent
@updateSync()
else unless @updateRequested
@updateRequested = true
- atom.views.updateDocument =>
+ @views.updateDocument =>
@updateRequested = false
@updateSync() if @canUpdate()
- atom.views.readDocument(@readAfterUpdateSync)
+ @views.readDocument(@readAfterUpdateSync)
canUpdate: ->
@mounted and @editor.isAlive()
@@ -275,13 +276,13 @@ class TextEditorComponent
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
observeConfig: ->
- @disposables.add atom.config.onDidChange 'editor.fontSize', =>
+ @disposables.add @config.onDidChange 'editor.fontSize', =>
@sampleFontStyling()
@invalidateCharacterWidths()
- @disposables.add atom.config.onDidChange 'editor.fontFamily', =>
+ @disposables.add @config.onDidChange 'editor.fontFamily', =>
@sampleFontStyling()
@invalidateCharacterWidths()
- @disposables.add atom.config.onDidChange 'editor.lineHeight', =>
+ @disposables.add @config.onDidChange 'editor.lineHeight', =>
@sampleFontStyling()
@invalidateCharacterWidths()
@@ -294,7 +295,7 @@ class TextEditorComponent
@disposables.add(@scopedConfigDisposables)
scope = @editor.getRootScopeDescriptor()
- @scopedConfigDisposables.add atom.config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity
+ @scopedConfigDisposables.add @config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity
focused: ->
if @mounted
@@ -354,11 +355,11 @@ class TextEditorComponent
{wheelDeltaX, wheelDeltaY} = event
# Ctrl+MouseWheel adjusts font size.
- if event.ctrlKey and atom.config.get('editor.zoomFontWhenCtrlScrolling')
+ if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling')
if wheelDeltaY > 0
- atom.workspace.increaseFontSize()
+ @workspace.increaseFontSize()
else if wheelDeltaY < 0
- atom.workspace.decreaseFontSize()
+ @workspace.decreaseFontSize()
event.preventDefault()
return
@@ -420,6 +421,9 @@ class TextEditorComponent
getScrollWidth: ->
@presenter.getScrollWidth()
+ getMaxScrollTop: ->
+ @presenter.getMaxScrollTop()
+
getVerticalScrollbarWidth: ->
@presenter.getVerticalScrollbarWidth()
@@ -543,7 +547,7 @@ class TextEditorComponent
onStylesheetsChanged: (styleElement) =>
return unless @performedInitialMeasurement
- return unless atom.themes.isInitialLoadComplete()
+ return unless @themes.isInitialLoadComplete()
# This delay prevents the styling from going haywire when stylesheets are
# reloaded in dev mode. It seems like a workaround for a browser bug, but
@@ -650,7 +654,7 @@ class TextEditorComponent
isVisible: ->
# Investigating an exception that occurs here due to ::domNode being null.
- atom.assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
+ @assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
error.metadata = {@initialized}
@domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0)
@@ -848,7 +852,7 @@ class TextEditorComponent
@presenter.characterWidthsChanged()
setShowIndentGuide: (showIndentGuide) ->
- atom.config.set("editor.showIndentGuide", showIndentGuide)
+ @config.set("editor.showIndentGuide", showIndentGuide)
setScrollSensitivity: (scrollSensitivity) =>
if scrollSensitivity = parseInt(scrollSensitivity)
diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee
index 405cf30e4..55e23d2da 100644
--- a/src/text-editor-element.coffee
+++ b/src/text-editor-element.coffee
@@ -4,6 +4,7 @@ Path = require 'path'
TextBuffer = require 'text-buffer'
TextEditor = require './text-editor'
TextEditorComponent = require './text-editor-component'
+StylesElement = require './styles-element'
ShadowStyleSheet = null
@@ -18,29 +19,38 @@ class TextEditorElement extends HTMLElement
logicalDisplayBuffer: true
createdCallback: ->
+ # Use globals when the following instance variables aren't set.
+ @config = atom.config
+ @themes = atom.themes
+ @workspace = atom.workspace
+ @assert = atom.assert
+ @views = atom.views
+ @styles = atom.styles
+ @grammars = atom.grammars
+
@emitter = new Emitter
@subscriptions = new CompositeDisposable
- @initializeContent()
+
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
- initializeContent: (attributes) ->
@classList.add('editor')
@setAttribute('tabindex', -1)
- if atom.config.get('editor.useShadowDOM')
+ initializeContent: (attributes) ->
+ if @config.get('editor.useShadowDOM')
@useShadowDOM = true
unless ShadowStyleSheet?
ShadowStyleSheet = document.createElement('style')
- ShadowStyleSheet.textContent = atom.themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less'))
+ ShadowStyleSheet.textContent = @themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less'))
@createShadowRoot()
@shadowRoot.appendChild(ShadowStyleSheet.cloneNode(true))
- @stylesElement = document.createElement('atom-styles')
+ @stylesElement = new StylesElement
+ @stylesElement.initialize(@styles)
@stylesElement.setAttribute('context', 'atom-text-editor')
- @stylesElement.initialize()
@rootElement = document.createElement('div')
@rootElement.classList.add('editor--private')
@@ -56,7 +66,7 @@ class TextEditorElement extends HTMLElement
attachedCallback: ->
@buildModel() unless @getModel()?
- atom.assert(@model.isAlive(), "Attaching a view for a destroyed editor")
+ @assert(@model.isAlive(), "Attaching a view for a destroyed editor")
@mountComponent() unless @component?
@listenForComponentEvents()
@component.checkForVisibilityChange()
@@ -76,7 +86,15 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
- initialize: (model) ->
+ initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
+ throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
+ throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
+ throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
+ throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
+ throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert?
+ throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
+ throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars?
+
@setModel(model)
this
@@ -85,6 +103,7 @@ class TextEditorElement extends HTMLElement
return if model.isDestroyed()
@model = model
+ @initializeContent()
@mountComponent()
@addGrammarScopeAttribute()
@addMiniAttribute() if @model.isMini()
@@ -99,7 +118,7 @@ class TextEditorElement extends HTMLElement
@model ? @buildModel()
buildModel: ->
- @setModel(new TextEditor(
+ @setModel(@workspace.buildTextEditor(
buffer: new TextBuffer(@textContent)
softWrapped: false
tabLength: 2
@@ -117,6 +136,12 @@ class TextEditorElement extends HTMLElement
editor: @model
tileSize: @tileSize
useShadowDOM: @useShadowDOM
+ views: @views
+ themes: @themes
+ config: @config
+ workspace: @workspace
+ assert: @assert
+ grammars: @grammars
)
@rootElement.appendChild(@component.getDomNode())
@@ -185,6 +210,12 @@ class TextEditorElement extends HTMLElement
getDefaultCharacterWidth: ->
@getModel().getDefaultCharWidth()
+ # Extended: Get the maximum scroll top that can be applied to this element.
+ #
+ # Returns a {Number} of pixels.
+ getMaxScrollTop: ->
+ @component?.getMaxScrollTop()
+
# Extended: Converts a buffer position to a pixel position.
#
# * `bufferPosition` An object that represents a buffer position. It can be either
@@ -313,141 +344,4 @@ class TextEditorElement extends HTMLElement
getHeight: ->
@offsetHeight
-stopEventPropagation = (commandListeners) ->
- newCommandListeners = {}
- for commandName, commandListener of commandListeners
- do (commandListener) ->
- newCommandListeners[commandName] = (event) ->
- event.stopPropagation()
- commandListener.call(@getModel(), event)
- newCommandListeners
-
-stopEventPropagationAndGroupUndo = (commandListeners) ->
- newCommandListeners = {}
- for commandName, commandListener of commandListeners
- do (commandListener) ->
- newCommandListeners[commandName] = (event) ->
- event.stopPropagation()
- model = @getModel()
- model.transact atom.config.get('editor.undoGroupingInterval'), ->
- commandListener.call(model, event)
- newCommandListeners
-
-atom.commands.add 'atom-text-editor', stopEventPropagation(
- 'core:undo': -> @undo()
- 'core:redo': -> @redo()
- 'core:move-left': -> @moveLeft()
- 'core:move-right': -> @moveRight()
- 'core:select-left': -> @selectLeft()
- 'core:select-right': -> @selectRight()
- 'core:select-up': -> @selectUp()
- 'core:select-down': -> @selectDown()
- 'core:select-all': -> @selectAll()
- 'editor:select-word': -> @selectWordsContainingCursors()
- 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections()
- 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph()
- 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph()
- 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine()
- 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine()
- 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine()
- 'editor:move-to-end-of-line': -> @moveToEndOfLine()
- 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine()
- 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord()
- 'editor:move-to-end-of-word': -> @moveToEndOfWord()
- 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord()
- 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary()
- 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary()
- 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary()
- 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary()
- 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph()
- 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph()
- 'editor:select-to-end-of-line': -> @selectToEndOfLine()
- 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine()
- 'editor:select-to-end-of-word': -> @selectToEndOfWord()
- 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord()
- 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord()
- 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary()
- 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary()
- 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary()
- 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary()
- 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine()
- 'editor:select-line': -> @selectLinesContainingCursors()
-)
-
-atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
- 'core:backspace': -> @backspace()
- 'core:delete': -> @delete()
- 'core:cut': -> @cutSelectedText()
- 'core:copy': -> @copySelectedText()
- 'core:paste': -> @pasteText()
- 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
- 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
- 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
- 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine()
- 'editor:delete-to-end-of-line': -> @deleteToEndOfLine()
- 'editor:delete-to-end-of-word': -> @deleteToEndOfWord()
- 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword()
- 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword()
- 'editor:delete-line': -> @deleteLine()
- 'editor:cut-to-end-of-line': -> @cutToEndOfLine()
- 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine()
- 'editor:transpose': -> @transpose()
- 'editor:upper-case': -> @upperCase()
- 'editor:lower-case': -> @lowerCase()
- 'editor:copy-selection': -> @copyOnlySelectedText()
-)
-
-atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation(
- 'core:move-up': -> @moveUp()
- 'core:move-down': -> @moveDown()
- 'core:move-to-top': -> @moveToTop()
- 'core:move-to-bottom': -> @moveToBottom()
- 'core:page-up': -> @pageUp()
- 'core:page-down': -> @pageDown()
- 'core:select-to-top': -> @selectToTop()
- 'core:select-to-bottom': -> @selectToBottom()
- 'core:select-page-up': -> @selectPageUp()
- 'core:select-page-down': -> @selectPageDown()
- 'editor:add-selection-below': -> @addSelectionBelow()
- 'editor:add-selection-above': -> @addSelectionAbove()
- 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines()
- 'editor:toggle-soft-tabs': -> @toggleSoftTabs()
- 'editor:toggle-soft-wrap': -> @toggleSoftWrapped()
- 'editor:fold-all': -> @foldAll()
- 'editor:unfold-all': -> @unfoldAll()
- 'editor:fold-current-row': -> @foldCurrentRow()
- 'editor:unfold-current-row': -> @unfoldCurrentRow()
- 'editor:fold-selection': -> @foldSelectedLines()
- 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0)
- 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1)
- 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2)
- 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3)
- 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4)
- 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5)
- 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
- 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
- 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
- 'editor:log-cursor-scope': -> @logCursorScope()
- 'editor:copy-path': -> @copyPathToClipboard()
- 'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide'))
- 'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers'))
- 'editor:scroll-to-cursor': -> @scrollToCursorPosition()
-)
-
-atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(
- 'editor:indent': -> @indent()
- 'editor:auto-indent': -> @autoIndentSelectedRows()
- 'editor:indent-selected-rows': -> @indentSelectedRows()
- 'editor:outdent-selected-rows': -> @outdentSelectedRows()
- 'editor:newline': -> @insertNewline()
- 'editor:newline-below': -> @insertNewlineBelow()
- 'editor:newline-above': -> @insertNewlineAbove()
- 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection()
- 'editor:checkout-head-revision': -> @checkoutHeadRevision()
- 'editor:move-line-up': -> @moveLineUp()
- 'editor:move-line-down': -> @moveLineDown()
- 'editor:duplicate-lines': -> @duplicateLines()
- 'editor:join-lines': -> @joinLines()
-)
-
module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
index 184d597cc..2e1d73c56 100644
--- a/src/text-editor-presenter.coffee
+++ b/src/text-editor-presenter.coffee
@@ -13,7 +13,7 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
- {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @scrollColumn, @scrollRow, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
+ {@model, @config, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @scrollColumn, @scrollRow, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
{horizontalScrollbarHeight, verticalScrollbarWidth} = params
{@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
@@ -22,6 +22,8 @@ class TextEditorPresenter
@gutterWidth ?= 0
@tileSize ?= 6
+ @realScrollTop = @scrollTop
+ @realScrollLeft = @scrollLeft
@disposables = new CompositeDisposable
@emitter = new Emitter
@visibleHighlights = {}
@@ -223,9 +225,9 @@ class TextEditorPresenter
observeConfig: ->
configParams = {scope: @model.getRootScopeDescriptor()}
- @scrollPastEnd = atom.config.get('editor.scrollPastEnd', configParams)
- @showLineNumbers = atom.config.get('editor.showLineNumbers', configParams)
- @showIndentGuide = atom.config.get('editor.showIndentGuide', configParams)
+ @scrollPastEnd = @config.get('editor.scrollPastEnd', configParams)
+ @showLineNumbers = @config.get('editor.showLineNumbers', configParams)
+ @showIndentGuide = @config.get('editor.showIndentGuide', configParams)
if @configDisposables?
@configDisposables?.dispose()
@@ -234,19 +236,19 @@ class TextEditorPresenter
@configDisposables = new CompositeDisposable
@disposables.add(@configDisposables)
- @configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
+ @configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
@showIndentGuide = newValue
@shouldUpdateContentState = true
@emitDidUpdateState()
- @configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
+ @configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@updateScrollHeight()
@emitDidUpdateState()
- @configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
+ @configDisposables.add @config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
@showLineNumbers = newValue
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@@ -775,7 +777,7 @@ class TextEditorPresenter
updateScrollTop: (scrollTop) ->
scrollTop = @constrainScrollTop(scrollTop)
- if scrollTop isnt @scrollTop and not Number.isNaN(scrollTop)
+ if scrollTop isnt @realScrollTop and not Number.isNaN(scrollTop)
@realScrollTop = scrollTop
@scrollTop = Math.round(scrollTop)
@scrollRow = Math.round(@scrollTop / @lineHeight)
@@ -792,7 +794,7 @@ class TextEditorPresenter
updateScrollLeft: (scrollLeft) ->
scrollLeft = @constrainScrollLeft(scrollLeft)
- if scrollLeft isnt @scrollLeft and not Number.isNaN(scrollLeft)
+ if scrollLeft isnt @realScrollLeft and not Number.isNaN(scrollLeft)
@realScrollLeft = scrollLeft
@scrollLeft = Math.round(scrollLeft)
@scrollColumn = Math.round(@scrollLeft / @baseCharacterWidth)
@@ -980,6 +982,13 @@ class TextEditorPresenter
getScrollWidth: ->
@scrollWidth
+ getMaxScrollTop: ->
+ scrollHeight = @getScrollHeight()
+ clientHeight = @getClientHeight()
+ return 0 unless scrollHeight? and clientHeight?
+
+ scrollHeight - clientHeight
+
setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight
oldHorizontalScrollbarHeight = @measuredHorizontalScrollbarHeight
@@ -1120,10 +1129,13 @@ class TextEditorPresenter
@mouseWheelScreenRow = screenRow
@didStartScrolling()
- setBaseCharacterWidth: (baseCharacterWidth) ->
- unless @baseCharacterWidth is baseCharacterWidth
+ setBaseCharacterWidth: (baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
+ unless @baseCharacterWidth is baseCharacterWidth and @doubleWidthCharWidth is doubleWidthCharWidth and @halfWidthCharWidth is halfWidthCharWidth and koreanCharWidth is @koreanCharWidth
@baseCharacterWidth = baseCharacterWidth
- @model.setDefaultCharWidth(baseCharacterWidth)
+ @doubleWidthCharWidth = doubleWidthCharWidth
+ @halfWidthCharWidth = halfWidthCharWidth
+ @koreanCharWidth = koreanCharWidth
+ @model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
@characterWidthsChanged()
characterWidthsChanged: ->
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
index a22b79205..e77ccb4a8 100644
--- a/src/text-editor.coffee
+++ b/src/text-editor.coffee
@@ -54,10 +54,7 @@ GutterContainer = require './gutter-container'
# soft wraps and folds to ensure your code interacts with them correctly.
module.exports =
class TextEditor extends Model
- atom.deserializers.add(this)
-
callDisplayBufferCreatedHook: false
- registerEditor: false
buffer: null
languageMode: null
cursors: null
@@ -67,9 +64,9 @@ class TextEditor extends Model
selectionFlashDuration: 500
gutterContainer: null
- @deserialize: (state) ->
+ @deserialize: (state, atomEnvironment) ->
try
- displayBuffer = DisplayBuffer.deserialize(state.displayBuffer)
+ displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment)
catch error
if error.syscall is 'read'
return # Error reading the file, don't deserialize an editor for it
@@ -77,19 +74,46 @@ class TextEditor extends Model
throw error
state.displayBuffer = displayBuffer
- state.registerEditor = true
+ state.config = atomEnvironment.config
+ state.notificationManager = atomEnvironment.notifications
+ state.packageManager = atomEnvironment.packages
+ state.clipboard = atomEnvironment.clipboard
+ state.viewRegistry = atomEnvironment.views
+ state.grammarRegistry = atomEnvironment.grammars
+ state.project = atomEnvironment.project
+ state.assert = atomEnvironment.assert.bind(atomEnvironment)
+ state.applicationDelegate = atomEnvironment.applicationDelegate
new this(state)
- constructor: ({@softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode}={}) ->
+ constructor: (params={}) ->
super
+ {
+ @softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength,
+ softWrapped, @displayBuffer, buffer, suppressCursorCreation, @mini, @placeholderText,
+ lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager,
+ @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
+ } = params
+
+ throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
+ throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager?
+ throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager?
+ throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard?
+ throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry?
+ throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry?
+ throw new Error("Must pass a project parameter when constructing TextEditors") unless @project?
+ throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert?
+
@emitter = new Emitter
@disposables = new CompositeDisposable
@cursors = []
@selections = []
buffer ?= new TextBuffer
- @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode})
+ @displayBuffer ?= new DisplayBuffer({
+ buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode,
+ @config, @assert, @grammarRegistry, @packageManager
+ })
@buffer = @displayBuffer.buffer
for marker in @findMarkers(@getSelectionMarkerAttributes())
@@ -105,9 +129,9 @@ class TextEditor extends Model
initialColumn = Math.max(parseInt(initialColumn) or 0, 0)
@addCursorAtBufferPosition([initialLine, initialColumn])
- @languageMode = new LanguageMode(this)
+ @languageMode = new LanguageMode(this, @config)
- @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor()))
+ @setEncoding(@config.get('core.fileEncoding', scope: @getRootScopeDescriptor()))
@gutterContainer = new GutterContainer(this)
@lineNumberGutter = @gutterContainer.addGutter
@@ -115,8 +139,6 @@ class TextEditor extends Model
priority: 0
visible: lineNumberGutterVisible
- atom.workspace?.editorAdded(this) if registerEditor
-
serialize: ->
deserializer: 'TextEditor'
id: @id
@@ -128,8 +150,8 @@ class TextEditor extends Model
subscribeToBuffer: ->
@buffer.retain()
@disposables.add @buffer.onDidChangePath =>
- unless atom.project.getPaths().length > 0
- atom.project.setPaths([path.dirname(@getPath())])
+ unless @project.getPaths().length > 0
+ @project.setPaths([path.dirname(@getPath())])
@emitter.emit 'did-change-title', @getTitle()
@emitter.emit 'did-change-path', @getPath()
@disposables.add @buffer.onDidChangeEncoding =>
@@ -148,7 +170,7 @@ class TextEditor extends Model
subscribeToTabTypeConfig: ->
@tabTypeSubscription?.dispose()
- @tabTypeSubscription = atom.config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
+ @tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
destroyed: ->
@@ -429,12 +451,12 @@ class TextEditor extends Model
onDidChangeScrollTop: (callback) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.")
- atom.views.getView(this).onDidChangeScrollTop(callback)
+ @viewRegistry.getView(this).onDidChangeScrollTop(callback)
onDidChangeScrollLeft: (callback) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.")
- atom.views.getView(this).onDidChangeScrollLeft(callback)
+ @viewRegistry.getView(this).onDidChangeScrollLeft(callback)
onDidRequestAutoscroll: (callback) ->
@displayBuffer.onDidRequestAutoscroll(callback)
@@ -456,7 +478,11 @@ class TextEditor extends Model
copy: ->
displayBuffer = @displayBuffer.copy()
softTabs = @getSoftTabs()
- newEditor = new TextEditor({@buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, registerEditor: true})
+ newEditor = new TextEditor({
+ @buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true,
+ @config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
+ @grammarRegistry, @project, @assert, @applicationDelegate
+ })
for marker in @findMarkers(editorId: @id)
marker.copy(editorId: newEditor.id, preserveFolds: true)
newEditor
@@ -547,6 +573,45 @@ class TextEditor extends Model
else
'untitled'
+ # Essential: Get unique title for display in other parts of the UI
+ # such as the window title.
+ #
+ # If the editor's buffer is unsaved, its title is "untitled"
+ # If the editor's buffer is saved, its unique title is formatted as one
+ # of the following,
+ # * "" when it is the only editing buffer with this file name.
+ # * "/.../", where the "..." may be omitted
+ # if the the direct parent directory is already different.
+ #
+ # Returns a {String}
+ getUniqueTitle: ->
+ if sessionPath = @getPath()
+ title = @getTitle()
+
+ # find text editors with identical file name.
+ paths = []
+ for textEditor in atom.workspace.getTextEditors() when textEditor isnt this
+ if textEditor.getTitle() is title
+ paths.push(textEditor.getPath())
+ if paths.length is 0
+ return title
+ fileName = path.basename(sessionPath)
+
+ # find the first directory in all these paths that is unique
+ nLevel = 0
+ while (_.some(paths, (apath) -> path.basename(apath) is path.basename(sessionPath)))
+ sessionPath = path.dirname(sessionPath)
+ paths = _.map(paths, (apath) -> path.dirname(apath))
+ nLevel += 1
+
+ directory = path.basename sessionPath
+ if nLevel > 1
+ path.join(directory, "...", fileName)
+ else
+ path.join(directory, fileName)
+ else
+ 'untitled'
+
# Essential: Get the editor's long title for display in other parts of the UI
# such as the window title.
#
@@ -557,7 +622,7 @@ class TextEditor extends Model
getLongTitle: ->
if sessionPath = @getPath()
fileName = path.basename(sessionPath)
- directory = atom.project.relativize(path.dirname(sessionPath))
+ directory = @project.relativize(path.dirname(sessionPath))
directory = if directory.length > 0 then directory else path.basename(path.dirname(sessionPath))
"#{fileName} - #{directory}"
else
@@ -585,7 +650,7 @@ class TextEditor extends Model
# Copies the current file path to the native clipboard.
copyPathToClipboard: ->
if filePath = @getPath()
- atom.clipboard.write(filePath)
+ @clipboard.write(filePath)
###
Section: File Operations
@@ -594,14 +659,14 @@ class TextEditor extends Model
# Essential: Saves the editor's text buffer.
#
# See {TextBuffer::save} for more details.
- save: -> @buffer.save(backup: atom.config.get('editor.backUpBeforeSaving'))
+ save: -> @buffer.save(backup: @config.get('editor.backUpBeforeSaving'))
# Essential: Saves the editor's text buffer as the given path.
#
# See {TextBuffer::saveAs} for more details.
#
# * `filePath` A {String} path.
- saveAs: (filePath) -> @buffer.saveAs(filePath, backup: atom.config.get('editor.backUpBeforeSaving'))
+ saveAs: (filePath) -> @buffer.saveAs(filePath, backup: @config.get('editor.backUpBeforeSaving'))
# Determine whether the user should be prompted to save before closing
# this editor.
@@ -617,9 +682,20 @@ class TextEditor extends Model
checkoutHeadRevision: ->
if filePath = this.getPath()
- atom.project.repositoryForDirectory(new Directory(path.dirname(filePath)))
- .then (repository) =>
- repository?.checkoutHeadForEditor(this)
+ checkoutHead = =>
+ @project.repositoryForDirectory(new Directory(path.dirname(filePath)))
+ .then (repository) =>
+ repository?.checkoutHeadForEditor(this)
+
+ if @config.get('editor.confirmCheckoutHeadRevision')
+ @applicationDelegate.confirm
+ message: 'Confirm Checkout HEAD Revision'
+ detailedMessage: "Are you sure you want to discard all changes to \"#{path.basename(filePath)}\" since the last Git commit?"
+ buttons:
+ OK: checkoutHead
+ Cancel: null
+ else
+ checkoutHead()
else
Promise.resolve(false)
@@ -748,7 +824,7 @@ class TextEditor extends Model
return false unless @emitWillInsertTextEvent(text)
groupingInterval = if options.groupUndo
- atom.config.get('editor.undoGroupingInterval')
+ @config.get('editor.undoGroupingInterval')
else
0
@@ -1765,7 +1841,7 @@ class TextEditor extends Model
# Add a cursor based on the given {Marker}.
addCursor: (marker) ->
- cursor = new Cursor(editor: this, marker: marker)
+ cursor = new Cursor(editor: this, marker: marker, config: @config)
@cursors.push(cursor)
@decorateMarker(marker, type: 'line-number', class: 'cursor-line')
@decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
@@ -2248,7 +2324,7 @@ class TextEditor extends Model
unless marker.getProperties().preserveFolds
@destroyFoldsContainingBufferRange(marker.getBufferRange())
cursor = @addCursor(marker)
- selection = new Selection(_.extend({editor: this, marker, cursor}, options))
+ selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options))
@selections.push(selection)
selectionBufferRange = selection.getBufferRange()
@mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds)
@@ -2405,10 +2481,10 @@ class TextEditor extends Model
#
# Returns a {Boolean}
shouldUseSoftTabs: ({defaultValue}) ->
- tabType = atom.config.get('editor.tabType', scope: @getRootScopeDescriptor())
+ tabType = @config.get('editor.tabType', scope: @getRootScopeDescriptor())
switch tabType
when 'auto'
- @usesSoftTabs() ? defaultValue ? atom.config.get('editor.softTabs') ? true
+ @usesSoftTabs() ? defaultValue ? @config.get('editor.softTabs') ? true
when 'hard'
false
when 'soft'
@@ -2584,7 +2660,7 @@ class TextEditor extends Model
list = list.map (item) -> "* #{item}"
content = "Scopes at Cursor\n#{list.join('\n')}"
- atom.notifications.addInfo(content, dismissable: true)
+ @notificationManager.addInfo(content, dismissable: true)
# {Delegates to: DisplayBuffer.tokenForBufferPosition}
tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
@@ -2636,7 +2712,7 @@ class TextEditor extends Model
#
# * `options` (optional) See {Selection::insertText}.
pasteText: (options={}) ->
- {text: clipboardText, metadata} = atom.clipboard.readWithMetadata()
+ {text: clipboardText, metadata} = @clipboard.readWithMetadata()
return false unless @emitWillInsertTextEvent(clipboardText)
metadata ?= {}
@@ -2889,24 +2965,24 @@ class TextEditor extends Model
scrollToTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
- atom.views.getView(this).scrollToTop()
+ @viewRegistry.getView(this).scrollToTop()
scrollToBottom: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
- atom.views.getView(this).scrollToBottom()
+ @viewRegistry.getView(this).scrollToBottom()
scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options)
getHorizontalScrollbarHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.")
- atom.views.getView(this).getHorizontalScrollbarHeight()
+ @viewRegistry.getView(this).getHorizontalScrollbarHeight()
getVerticalScrollbarWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.")
- atom.views.getView(this).getVerticalScrollbarWidth()
+ @viewRegistry.getView(this).getVerticalScrollbarWidth()
pageUp: ->
@moveUp(@getRowsPerPage())
@@ -2931,10 +3007,10 @@ class TextEditor extends Model
###
shouldAutoIndent: ->
- atom.config.get("editor.autoIndent", scope: @getRootScopeDescriptor())
+ @config.get("editor.autoIndent", scope: @getRootScopeDescriptor())
shouldAutoIndentOnPaste: ->
- atom.config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor())
+ @config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor())
###
Section: Event Handlers
@@ -2973,19 +3049,19 @@ class TextEditor extends Model
getFirstVisibleScreenRow: ->
deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.")
- atom.views.getView(this).getVisibleRowRange()[0]
+ @viewRegistry.getView(this).getVisibleRowRange()[0]
getLastVisibleScreenRow: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.")
- atom.views.getView(this).getVisibleRowRange()[1]
+ @viewRegistry.getView(this).getVisibleRowRange()[1]
pixelPositionForBufferPosition: (bufferPosition) ->
Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead")
- atom.views.getView(this).pixelPositionForBufferPosition(bufferPosition)
+ @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition)
pixelPositionForScreenPosition: (screenPosition) ->
Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead")
- atom.views.getView(this).pixelPositionForScreenPosition(screenPosition)
+ @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition)
getSelectionMarkerAttributes: ->
{type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true}
@@ -2999,15 +3075,22 @@ class TextEditor extends Model
getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
+ getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth()
+
+ getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth()
+
+ getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth()
+
getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
- setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)
+ setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
+ @displayBuffer.setDefaultCharWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
setHeight: (height, reentrant=false) ->
if reentrant
@displayBuffer.setHeight(height)
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
- atom.views.getView(this).setHeight(height)
+ @viewRegistry.getView(this).setHeight(height)
getHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
@@ -3020,7 +3103,7 @@ class TextEditor extends Model
@displayBuffer.setWidth(width)
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
- atom.views.getView(this).setWidth(width)
+ @viewRegistry.getView(this).setWidth(width)
getWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
@@ -3035,77 +3118,82 @@ class TextEditor extends Model
getScrollTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.")
- atom.views.getView(this).getScrollTop()
+ @viewRegistry.getView(this).getScrollTop()
setScrollTop: (scrollTop) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.")
- atom.views.getView(this).setScrollTop(scrollTop)
+ @viewRegistry.getView(this).setScrollTop(scrollTop)
getScrollBottom: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.")
- atom.views.getView(this).getScrollBottom()
+ @viewRegistry.getView(this).getScrollBottom()
setScrollBottom: (scrollBottom) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.")
- atom.views.getView(this).setScrollBottom(scrollBottom)
+ @viewRegistry.getView(this).setScrollBottom(scrollBottom)
getScrollLeft: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.")
- atom.views.getView(this).getScrollLeft()
+ @viewRegistry.getView(this).getScrollLeft()
setScrollLeft: (scrollLeft) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.")
- atom.views.getView(this).setScrollLeft(scrollLeft)
+ @viewRegistry.getView(this).setScrollLeft(scrollLeft)
getScrollRight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.")
- atom.views.getView(this).getScrollRight()
+ @viewRegistry.getView(this).getScrollRight()
setScrollRight: (scrollRight) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.")
- atom.views.getView(this).setScrollRight(scrollRight)
+ @viewRegistry.getView(this).setScrollRight(scrollRight)
getScrollHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.")
- atom.views.getView(this).getScrollHeight()
+ @viewRegistry.getView(this).getScrollHeight()
getScrollWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.")
- atom.views.getView(this).getScrollWidth()
+ @viewRegistry.getView(this).getScrollWidth()
+
+ getMaxScrollTop: ->
+ Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.")
+
+ @viewRegistry.getView(this).getMaxScrollTop()
getVisibleRowRange: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getVisibleRowRange instead.")
- atom.views.getView(this).getVisibleRowRange()
+ @viewRegistry.getView(this).getVisibleRowRange()
intersectsVisibleRowRange: (startRow, endRow) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.")
- atom.views.getView(this).intersectsVisibleRowRange(startRow, endRow)
+ @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow)
selectionIntersectsVisibleRowRange: (selection) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.")
- atom.views.getView(this).selectionIntersectsVisibleRowRange(selection)
+ @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection)
screenPositionForPixelPosition: (pixelPosition) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.")
- atom.views.getView(this).screenPositionForPixelPosition(pixelPosition)
+ @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition)
pixelRectForScreenRange: (screenRange) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.")
- atom.views.getView(this).pixelRectForScreenRange(screenRange)
+ @viewRegistry.getView(this).pixelRectForScreenRange(screenRange)
###
Section: Utility
diff --git a/src/text-utils.coffee b/src/text-utils.coffee
index 37955dcd6..82bed4da5 100644
--- a/src/text-utils.coffee
+++ b/src/text-utils.coffee
@@ -57,6 +57,38 @@ isPairedCharacter = (string, index=0) ->
isVariationSequence(charCodeA, charCodeB) or
isCombinedCharacter(charCodeA, charCodeB)
+isJapaneseCharacter = (charCode) ->
+ 0x3000 <= charCode <= 0x30FF
+
+isCjkUnifiedIdeograph = (charCode) ->
+ 0x4E00 <= charCode <= 0x9FAF
+
+isFullWidthForm = (charCode) ->
+ 0xFF01 <= charCode <= 0xFF5E or
+ 0xFFE0 <= charCode <= 0xFFE6
+
+isDoubleWidthCharacter = (character) ->
+ charCode = character.charCodeAt(0)
+
+ isJapaneseCharacter(charCode) or
+ isCjkUnifiedIdeograph(charCode) or
+ isFullWidthForm(charCode)
+
+isHalfWidthCharacter = (character) ->
+ charCode = character.charCodeAt(0)
+
+ 0xFF65 <= charCode <= 0xFFDC or
+ 0xFFE8 <= charCode <= 0xFFEE
+
+isKoreanCharacter = (character) ->
+ charCode = character.charCodeAt(0)
+
+ 0xAC00 <= charCode <= 0xD7A3 or
+ 0x1100 <= charCode <= 0x11FF or
+ 0x3130 <= charCode <= 0x318F or
+ 0xA960 <= charCode <= 0xA97F or
+ 0xD7B0 <= charCode <= 0xD7FF
+
# Does the given string contain at least surrogate pair, variation sequence,
# or combined character?
#
@@ -70,4 +102,4 @@ hasPairedCharacter = (string) ->
index++
false
-module.exports = {isPairedCharacter, hasPairedCharacter}
+module.exports = {isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter}
diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee
index 0d329cea7..649478841 100644
--- a/src/theme-manager.coffee
+++ b/src/theme-manager.coffee
@@ -9,12 +9,14 @@ fs = require 'fs-plus'
# An instance of this class is always available as the `atom.themes` global.
module.exports =
class ThemeManager
- constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) ->
+ constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode, @config, @styleManager, @notificationManager, @viewRegistry}) ->
@emitter = new Emitter
@styleSheetDisposablesBySourcePath = {}
@lessCache = null
@initialLoadComplete = false
@packageManager.registerPackageActivator(this, ['theme'])
+ @packageManager.onDidActivateInitialPackages =>
+ @onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets()
###
Section: Event Subscription
@@ -66,21 +68,21 @@ class ThemeManager
###
warnForNonExistentThemes: ->
- themeNames = atom.config.get('core.themes') ? []
+ themeNames = @config.get('core.themes') ? []
themeNames = [themeNames] unless _.isArray(themeNames)
for themeName in themeNames
- unless themeName and typeof themeName is 'string' and atom.packages.resolvePackagePath(themeName)
+ unless themeName and typeof themeName is 'string' and @packageManager.resolvePackagePath(themeName)
console.warn("Enabled theme '#{themeName}' is not installed.")
# Public: Get the enabled theme names from the config.
#
# Returns an array of theme names in the order that they should be activated.
getEnabledThemeNames: ->
- themeNames = atom.config.get('core.themes') ? []
+ themeNames = @config.get('core.themes') ? []
themeNames = [themeNames] unless _.isArray(themeNames)
- themeNames = themeNames.filter (themeName) ->
+ themeNames = themeNames.filter (themeName) =>
if themeName and typeof themeName is 'string'
- return true if atom.packages.resolvePackagePath(themeName)
+ return true if @packageManager.resolvePackagePath(themeName)
false
# Use a built-in syntax and UI theme any time the configured themes are not
@@ -139,7 +141,7 @@ class ThemeManager
loadUserStylesheet: ->
@unwatchUserStylesheet()
- userStylesheetPath = atom.styles.getUserStyleSheetPath()
+ userStylesheetPath = @styleManager.getUserStyleSheetPath()
return unless fs.isFileSync(userStylesheetPath)
try
@@ -158,14 +160,14 @@ class ThemeManager
[this document][watches] for more info.
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
"""
- atom.notifications.addError(message, dismissable: true)
+ @notificationManager.addError(message, dismissable: true)
try
userStylesheetContents = @loadStylesheet(userStylesheetPath, true)
catch
return
- @userStyleSheetDisposable = atom.styles.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2)
+ @userStyleSheetDisposable = @styleManager.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2)
loadBaseStylesheets: ->
@requireStylesheet('../static/bootstrap')
@@ -221,22 +223,22 @@ class ThemeManager
message = "Error loading Less stylesheet: `#{lessStylesheetPath}`"
detail = error.message
- atom.notifications.addError(message, {detail, dismissable: true})
+ @notificationManager.addError(message, {detail, dismissable: true})
throw error
removeStylesheet: (stylesheetPath) ->
@styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose()
applyStylesheet: (path, text) ->
- @styleSheetDisposablesBySourcePath[path] = atom.styles.addStyleSheet(text, sourcePath: path)
+ @styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet(text, sourcePath: path)
stringToId: (string) ->
string.replace(/\\/g, '/')
activateThemes: ->
new Promise (resolve) =>
- # atom.config.observe runs the callback once, then on subsequent changes.
- atom.config.observe 'core.themes', =>
+ # @config.observe runs the callback once, then on subsequent changes.
+ @config.observe 'core.themes', =>
@deactivateThemes()
@warnForNonExistentThemes()
@@ -268,13 +270,13 @@ class ThemeManager
isInitialLoadComplete: -> @initialLoadComplete
addActiveThemeClasses: ->
- if workspaceElement = atom.views.getView(atom.workspace)
+ if workspaceElement = @viewRegistry.getView(@workspace)
for pack in @getActiveThemes()
workspaceElement.classList.add("theme-#{pack.name}")
return
removeActiveThemeClasses: ->
- workspaceElement = atom.views.getView(atom.workspace)
+ workspaceElement = @viewRegistry.getView(@workspace)
for pack in @getActiveThemes()
workspaceElement.classList.remove("theme-#{pack.name}")
return
diff --git a/src/theme-package.coffee b/src/theme-package.coffee
index 6d0a88052..084728869 100644
--- a/src/theme-package.coffee
+++ b/src/theme-package.coffee
@@ -7,10 +7,10 @@ class ThemePackage extends Package
getStyleSheetPriority: -> 1
enable: ->
- atom.config.unshiftAtKeyPath('core.themes', @name)
+ @config.unshiftAtKeyPath('core.themes', @name)
disable: ->
- atom.config.removeAtKeyPath('core.themes', @name)
+ @config.removeAtKeyPath('core.themes', @name)
load: ->
@loadTime = 0
diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee
index de874d499..8f0fe202f 100644
--- a/src/token-iterator.coffee
+++ b/src/token-iterator.coffee
@@ -1,19 +1,18 @@
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
+{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils'
module.exports =
class TokenIterator
- constructor: (line) ->
- @reset(line) if line?
+ constructor: ({@grammarRegistry}, line, enableScopes) ->
+ @reset(line, enableScopes) if line?
- reset: (@line) ->
+ reset: (@line, @enableScopes=true) ->
@index = null
@bufferStart = @line.startBufferColumn
@bufferEnd = @bufferStart
@screenStart = 0
@screenEnd = 0
- @scopes = @line.openScopes.map (id) -> atom.grammars.scopeForId(id)
- @scopeStarts = @scopes.slice()
- @scopeEnds = []
+ @resetScopes() if @enableScopes
this
next: ->
@@ -21,26 +20,16 @@ class TokenIterator
if @index?
@index++
- @scopeEnds.length = 0
- @scopeStarts.length = 0
@bufferStart = @bufferEnd
@screenStart = @screenEnd
+ @clearScopeStartsAndEnds() if @enableScopes
else
@index = 0
while @index < tags.length
tag = tags[@index]
if tag < 0
- scope = atom.grammars.scopeForId(tag)
- if tag % 2 is 0
- if @scopeStarts[@scopeStarts.length - 1] is scope
- @scopeStarts.pop()
- else
- @scopeEnds.push(scope)
- @scopes.pop()
- else
- @scopeStarts.push(scope)
- @scopes.push(scope)
+ @handleScopeForTag(tag) if @enableScopes
@index++
else
if @isHardTab()
@@ -52,10 +41,33 @@ class TokenIterator
else
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + tag
+
+ @text = @line.text.substring(@screenStart, @screenEnd)
return true
false
+ resetScopes: ->
+ @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id)
+ @scopeStarts = @scopes.slice()
+ @scopeEnds = []
+
+ clearScopeStartsAndEnds: ->
+ @scopeEnds.length = 0
+ @scopeStarts.length = 0
+
+ handleScopeForTag: (tag) ->
+ scope = @grammarRegistry.scopeForId(tag)
+ if tag % 2 is 0
+ if @scopeStarts[@scopeStarts.length - 1] is scope
+ @scopeStarts.pop()
+ else
+ @scopeEnds.push(scope)
+ @scopes.pop()
+ else
+ @scopeStarts.push(scope)
+ @scopes.push(scope)
+
getBufferStart: -> @bufferStart
getBufferEnd: -> @bufferEnd
@@ -67,8 +79,7 @@ class TokenIterator
getScopes: -> @scopes
- getText: ->
- @line.text.substring(@screenStart, @screenEnd)
+ getText: -> @text
isSoftTab: ->
@line.specialTokens[@index] is SoftTab
@@ -82,5 +93,14 @@ class TokenIterator
isPairedCharacter: ->
@line.specialTokens[@index] is PairedCharacter
+ hasDoubleWidthCharacterAt: (charIndex) ->
+ isDoubleWidthCharacter(@getText()[charIndex])
+
+ hasHalfWidthCharacterAt: (charIndex) ->
+ isHalfWidthCharacter(@getText()[charIndex])
+
+ hasKoreanCharacterAt: (charIndex) ->
+ isKoreanCharacter(@getText()[charIndex])
+
isAtomic: ->
@isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter()
diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee
index 8721b0547..2df29a31c 100644
--- a/src/tokenized-buffer.coffee
+++ b/src/tokenized-buffer.coffee
@@ -21,17 +21,26 @@ class TokenizedBuffer extends Model
configSettings: null
changeCount: 0
- @deserialize: (state) ->
- state.buffer = atom.project.bufferForPathSync(state.bufferPath)
+ @deserialize: (state, atomEnvironment) ->
+ state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
+ state.config = atomEnvironment.config
+ state.grammarRegistry = atomEnvironment.grammars
+ state.packageManager = atomEnvironment.packages
+ state.assert = atomEnvironment.assert
new this(state)
- constructor: ({@buffer, @tabLength, @ignoreInvisibles, @largeFileMode}) ->
+ constructor: (params) ->
+ {
+ @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config,
+ @grammarRegistry, @packageManager, @assert
+ } = params
+
@emitter = new Emitter
@disposables = new CompositeDisposable
- @tokenIterator = new TokenIterator
+ @tokenIterator = new TokenIterator({@grammarRegistry})
- @disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
- @disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
+ @disposables.add @grammarRegistry.onDidAddGrammar(@grammarAddedOrUpdated)
+ @disposables.add @grammarRegistry.onDidUpdateGrammar(@grammarAddedOrUpdated)
@disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e)
@disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()
@@ -65,7 +74,7 @@ class TokenizedBuffer extends Model
if grammar.injectionSelector?
@retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
else
- newScore = atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
+ newScore = @grammarRegistry.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
setGrammar: (grammar, score) ->
@@ -73,7 +82,7 @@ class TokenizedBuffer extends Model
@grammar = grammar
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
- @currentGrammarScore = score ? atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
+ @currentGrammarScore = score ? @grammarRegistry.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
@grammarUpdateDisposable?.dispose()
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
@@ -81,33 +90,33 @@ class TokenizedBuffer extends Model
scopeOptions = {scope: @rootScopeDescriptor}
@configSettings =
- tabLength: atom.config.get('editor.tabLength', scopeOptions)
- invisibles: atom.config.get('editor.invisibles', scopeOptions)
- showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions)
+ tabLength: @config.get('editor.tabLength', scopeOptions)
+ invisibles: @config.get('editor.invisibles', scopeOptions)
+ showInvisibles: @config.get('editor.showInvisibles', scopeOptions)
if @configSubscriptions?
@configSubscriptions.dispose()
@disposables.remove(@configSubscriptions)
@configSubscriptions = new CompositeDisposable
- @configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
+ @configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
@configSettings.tabLength = newValue
@retokenizeLines()
['invisibles', 'showInvisibles'].forEach (key) =>
- @configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
+ @configSubscriptions.add @config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
oldInvisibles = @getInvisiblesToShow()
@configSettings[key] = newValue
@retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
@disposables.add(@configSubscriptions)
@retokenizeLines()
- atom.packages.triggerActivationHook("#{grammar.packageName}:grammar-used")
+ @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used")
@emitter.emit 'did-change-grammar', grammar
getGrammarSelectionContent: ->
@buffer.getTextInRange([[0, 0], [10, 0]])
reloadGrammar: ->
- if grammar = atom.grammars.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent())
+ if grammar = @grammarRegistry.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent())
@setGrammar(grammar)
else
throw new Error("No grammar found for path: #{path}")
@@ -155,7 +164,7 @@ class TokenizedBuffer extends Model
tokenizeNextChunk: ->
# Short circuit null grammar which can just use the placeholder tokens
- if @grammar is atom.grammars.nullGrammar and @firstInvalidRow()?
+ if @grammar is @grammarRegistry.nullGrammar and @firstInvalidRow()?
@invalidRows = []
@markTokenizationComplete()
return
@@ -291,7 +300,7 @@ class TokenizedBuffer extends Model
# undefined. This should paper over the problem but we want to figure out
# what is happening:
tokenizedLine = @tokenizedLineForRow(row)
- atom.assert tokenizedLine?, "TokenizedLine is undefined", (error) =>
+ @assert tokenizedLine?, "TokenizedLine is undefined", (error) =>
error.metadata = {
row: row
rowCount: @tokenizedLines.length
@@ -391,7 +400,7 @@ class TokenizedBuffer extends Model
loop
break if scopes.pop() is matchingStartTag
if scopes.length is 0
- atom.assert false, "Encountered an unmatched scope end tag.", (error) =>
+ @assert false, "Encountered an unmatched scope end tag.", (error) =>
error.metadata = {
grammarScopeName: @grammar.scopeName
unmatchedEndTag: @grammar.scopeForId(tag)
@@ -472,13 +481,13 @@ class TokenizedBuffer extends Model
position = Point.fromObject(position)
{openScopes, tags} = @tokenizedLineForRow(position.row)
- scopes = openScopes.map (tag) -> atom.grammars.scopeForId(tag)
+ scopes = openScopes.map (tag) => @grammarRegistry.scopeForId(tag)
startColumn = 0
for tag, tokenIndex in tags
if tag < 0
if tag % 2 is -1
- scopes.push(atom.grammars.scopeForId(tag))
+ scopes.push(@grammarRegistry.scopeForId(tag))
else
scopes.pop()
else
@@ -498,7 +507,7 @@ class TokenizedBuffer extends Model
if tag % 2 is -1
startScopes.pop()
else
- startScopes.push(atom.grammars.scopeForId(tag))
+ startScopes.push(@grammarRegistry.scopeForId(tag))
else
break unless selectorMatchesAnyScope(selector, startScopes)
startColumn -= tag
@@ -508,7 +517,7 @@ class TokenizedBuffer extends Model
tag = tags[endTokenIndex]
if tag < 0
if tag % 2 is -1
- endScopes.push(atom.grammars.scopeForId(tag))
+ endScopes.push(@grammarRegistry.scopeForId(tag))
else
endScopes.pop()
else
diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee
index bf234b6d3..2a27d3f12 100644
--- a/src/tokenized-line.coffee
+++ b/src/tokenized-line.coffee
@@ -184,7 +184,7 @@ class TokenizedLine
@lineIsWhitespaceOnly = true
@firstTrailingWhitespaceIndex = 0
- getTokenIterator: -> @tokenIterator.reset(this)
+ getTokenIterator: -> @tokenIterator.reset(this, arguments...)
Object.defineProperty @prototype, 'tokens', get: ->
iterator = @getTokenIterator()
diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee
index 21b8b07c6..247437535 100644
--- a/src/tooltip-manager.coffee
+++ b/src/tooltip-manager.coffee
@@ -54,6 +54,8 @@ class TooltipManager
placement: 'auto top'
viewportPadding: 2
+ constructor: ({@keymapManager}) ->
+
# Essential: Add a tooltip to the given element.
#
# * `target` An `HTMLElement`
@@ -81,7 +83,7 @@ class TooltipManager
{keyBindingCommand, keyBindingTarget} = options
if keyBindingCommand?
- bindings = atom.keymaps.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget)
+ bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget)
keystroke = getKeystroke(bindings)
if options.title? and keystroke?
options.title += " " + getKeystroke(bindings)
diff --git a/src/view-registry.coffee b/src/view-registry.coffee
index 6af7bd024..3a46aa87a 100644
--- a/src/view-registry.coffee
+++ b/src/view-registry.coffee
@@ -49,15 +49,15 @@ class ViewRegistry
debouncedPerformDocumentPoll: null
minimumPollInterval: 200
- constructor: ->
+ constructor: (@atomEnvironment) ->
+ @observer = new MutationObserver(@requestDocumentPoll)
+ @clear()
+
+ clear: ->
@views = new WeakMap
@providers = []
- @documentWriters = []
- @documentReaders = []
- @documentPollers = []
-
- @observer = new MutationObserver(@requestDocumentPoll)
@debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this)
+ @clearDocumentRequests()
# Essential: Add a provider that will be used to construct views in the
# workspace's view layer based on model objects in its model layer.
@@ -159,7 +159,7 @@ class ViewRegistry
else if object?.jquery
object[0]
else if provider = @findProvider(object)
- element = provider.createView?(object)
+ element = provider.createView?(object, @atomEnvironment)
unless element?
element = new provider.viewConstructor
element.initialize?(object) ? element.setModel?(object)
diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee
deleted file mode 100644
index 886ba26dc..000000000
--- a/src/window-bootstrap.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-# Like sands through the hourglass, so are the days of our lives.
-require './window'
-
-Atom = require './atom'
-window.atom = Atom.loadOrCreate('editor')
-atom.initialize()
-atom.startEditorWindow()
-
-# Workaround for focus getting cleared upon window creation
-windowFocused = ->
- window.removeEventListener('focus', windowFocused)
- setTimeout (-> document.querySelector('atom-workspace').focus()), 0
-window.addEventListener('focus', windowFocused)
diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee
index 270650b2f..c786f7e9c 100644
--- a/src/window-event-handler.coffee
+++ b/src/window-event-handler.coffee
@@ -1,44 +1,38 @@
path = require 'path'
{Disposable, CompositeDisposable} = require 'event-kit'
-ipc = require 'ipc'
-shell = require 'shell'
fs = require 'fs-plus'
listen = require './delegated-listener'
-# Handles low-level events related to the window.
+# Handles low-level events related to the @window.
module.exports =
class WindowEventHandler
- constructor: ->
+ constructor: ({@atomEnvironment, @applicationDelegate, @window, @document}) ->
@reloadRequested = false
@subscriptions = new CompositeDisposable
- @on(ipc, 'message', @handleIPCMessage)
- @on(ipc, 'command', @handleIPCCommand)
- @on(ipc, 'context-command', @handleIPCContextCommand)
+ @previousOnbeforeunloadHandler = @window.onbeforeunload
+ @window.onbeforeunload = @handleWindowBeforeunload
+ @addEventListener(@window, 'focus', @handleWindowFocus)
+ @addEventListener(@window, 'blur', @handleWindowBlur)
- @addEventListener(window, 'focus', @handleWindowFocus)
- @addEventListener(window, 'blur', @handleWindowBlur)
- @addEventListener(window, 'beforeunload', @handleWindowBeforeunload)
- @addEventListener(window, 'unload', @handleWindowUnload)
+ @addEventListener(@document, 'keydown', @handleDocumentKeydown)
+ @addEventListener(@document, 'drop', @handleDocumentDrop)
+ @addEventListener(@document, 'dragover', @handleDocumentDragover)
+ @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
+ @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick)
+ @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit)
- @addEventListener(document, 'keydown', @handleDocumentKeydown)
- @addEventListener(document, 'drop', @handleDocumentDrop)
- @addEventListener(document, 'dragover', @handleDocumentDragover)
- @addEventListener(document, 'contextmenu', @handleDocumentContextmenu)
- @subscriptions.add listen(document, 'click', 'a', @handleLinkClick)
- @subscriptions.add listen(document, 'submit', 'form', @handleFormSubmit)
-
- @subscriptions.add atom.commands.add window,
+ @subscriptions.add @atomEnvironment.commands.add @window,
'window:toggle-full-screen': @handleWindowToggleFullScreen
'window:close': @handleWindowClose
'window:reload': @handleWindowReload
'window:toggle-dev-tools': @handleWindowToggleDevTools
if process.platform in ['win32', 'linux']
- @subscriptions.add atom.commands.add window,
+ @subscriptions.add @atomEnvironment.commands.add @window,
'window:toggle-menu-bar': @handleWindowToggleMenuBar
- @subscriptions.add atom.commands.add document,
+ @subscriptions.add @atomEnvironment.commands.add @document,
'core:focus-next': @handleFocusNext
'core:focus-previous': @handleFocusPrevious
@@ -48,9 +42,9 @@ class WindowEventHandler
# `.native-key-bindings` class.
handleNativeKeybindings: ->
bindCommandToAction = (command, action) =>
- @addEventListener document, command, (event) ->
+ @addEventListener @document, command, (event) =>
if event.target.webkitMatchesSelector('.native-key-bindings')
- atom.getCurrentWindow().webContents[action]()
+ @applicationDelegate.getCurrentWindow().webContents[action]()
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:paste', 'paste')
@@ -60,6 +54,7 @@ class WindowEventHandler
bindCommandToAction('core:cut', 'cut')
unsubscribe: ->
+ @window.onbeforeunload = @previousOnbeforeunloadHandler
@subscriptions.dispose()
on: (target, eventName, handler) ->
@@ -72,8 +67,8 @@ class WindowEventHandler
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
- handleDocumentKeydown: (event) ->
- atom.keymaps.handleKeyboardEvent(event)
+ handleDocumentKeydown: (event) =>
+ @atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
handleDrop: (event) ->
@@ -86,14 +81,14 @@ class WindowEventHandler
event.dataTransfer.dropEffect = 'none'
eachTabIndexedElement: (callback) ->
- for element in document.querySelectorAll('[tabindex]')
+ for element in @document.querySelectorAll('[tabindex]')
continue if element.disabled
continue unless element.tabIndex >= 0
callback(element, element.tabIndex)
return
handleFocusNext: =>
- focusedTabIndex = document.activeElement.tabIndex ? -Infinity
+ focusedTabIndex = @document.activeElement.tabIndex ? -Infinity
nextElement = null
nextTabIndex = Infinity
@@ -114,7 +109,7 @@ class WindowEventHandler
lowestElement.focus()
handleFocusPrevious: =>
- focusedTabIndex = document.activeElement.tabIndex ? Infinity
+ focusedTabIndex = @document.activeElement.tabIndex ? Infinity
previousElement = null
previousTabIndex = -Infinity
@@ -134,91 +129,61 @@ class WindowEventHandler
else if highestElement?
highestElement.focus()
- handleIPCMessage: (message, detail) ->
- switch message
- when 'open-locations'
- needsProjectPaths = atom.project?.getPaths().length is 0
-
- for {pathToOpen, initialLine, initialColumn} in detail
- if pathToOpen? and needsProjectPaths
- if fs.existsSync(pathToOpen)
- atom.project.addPath(pathToOpen)
- else if fs.existsSync(path.dirname(pathToOpen))
- atom.project.addPath(path.dirname(pathToOpen))
- else
- atom.project.addPath(pathToOpen)
-
- unless fs.isDirectorySync(pathToOpen)
- atom.workspace?.open(pathToOpen, {initialLine, initialColumn})
- return
- when 'update-available'
- atom.updateAvailable(detail)
-
- handleIPCCommand: (command, args...) ->
- activeElement = document.activeElement
- # Use the workspace element view if body has focus
- if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace)
- activeElement = workspaceElement
-
- atom.commands.dispatch(activeElement, command, args[0])
-
- handleIPCContextCommand: (command, args...) ->
- atom.commands.dispatch(atom.contextMenu.activeElement, command, args)
-
handleWindowFocus: ->
- document.body.classList.remove('is-blurred')
+ @document.body.classList.remove('is-blurred')
- handleWindowBlur: ->
- document.body.classList.add('is-blurred')
- atom.storeDefaultWindowDimensions()
+ handleWindowBlur: =>
+ @document.body.classList.add('is-blurred')
+ @atomEnvironment.storeDefaultWindowDimensions()
handleWindowBeforeunload: =>
- confirmed = atom.workspace?.confirmClose(windowCloseRequested: true)
- atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused()
+ confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true)
+ if confirmed and not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused()
+ @atomEnvironment.hide()
@reloadRequested = false
- atom.storeDefaultWindowDimensions()
- atom.storeWindowDimensions()
+ @atomEnvironment.storeDefaultWindowDimensions()
+ @atomEnvironment.storeWindowDimensions()
if confirmed
- atom.unloadEditorWindow()
+ @atomEnvironment.unloadEditorWindow()
else
- ipc.send('cancel-window-close')
+ @applicationDelegate.didCancelWindowUnload()
confirmed
- handleWindowUnload: ->
- atom.removeEditorWindow()
+ handleWindowUnload: =>
+ @atomEnvironment.destroy()
- handleWindowToggleFullScreen: ->
- atom.toggleFullScreen()
+ handleWindowToggleFullScreen: =>
+ @atomEnvironment.toggleFullScreen()
- handleWindowClose: ->
- atom.close()
+ handleWindowClose: =>
+ @atomEnvironment.close()
- handleWindowReload: ->
+ handleWindowReload: =>
@reloadRequested = true
- atom.reload()
+ @atomEnvironment.reload()
- handleWindowToggleDevTools: ->
- atom.toggleDevTools()
+ handleWindowToggleDevTools: =>
+ @atomEnvironment.toggleDevTools()
- handleWindowToggleMenuBar: ->
- atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
+ handleWindowToggleMenuBar: =>
+ @atomEnvironment.config.set('core.autoHideMenuBar', not @atomEnvironment.config.get('core.autoHideMenuBar'))
- if atom.config.get('core.autoHideMenuBar')
+ if @atomEnvironment.config.get('core.autoHideMenuBar')
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
- atom.notifications.addInfo('Menu bar hidden', {detail})
+ @atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
- handleLinkClick: (event) ->
+ handleLinkClick: (event) =>
event.preventDefault()
- location = event.currentTarget?.getAttribute('href')
- if location and location[0] isnt '#' and /^https?:\/\//.test(location)
- shell.openExternal(location)
+ uri = event.currentTarget?.getAttribute('href')
+ if uri and uri[0] isnt '#' and /^https?:\/\//.test(uri)
+ @applicationDelegate.openExternal(uri)
handleFormSubmit: (event) ->
# Prevent form submits from changing the current window's URL
event.preventDefault()
- handleDocumentContextmenu: (event) ->
+ handleDocumentContextmenu: (event) =>
event.preventDefault()
- atom.contextMenu.showForEvent(event)
+ @atomEnvironment.contextMenu.showForEvent(event)
diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee
new file mode 100644
index 000000000..59ee2f382
--- /dev/null
+++ b/src/window-load-settings-helpers.coffee
@@ -0,0 +1,20 @@
+remote = require 'remote'
+_ = require 'underscore-plus'
+
+windowLoadSettings = null
+
+exports.getWindowLoadSettings = ->
+ windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1)))
+ clone = _.deepClone(windowLoadSettings)
+
+ # The windowLoadSettings.windowState could be large, request it only when needed.
+ clone.__defineGetter__ 'windowState', ->
+ remote.getCurrentWindow().loadSettings.windowState
+ clone.__defineSetter__ 'windowState', (value) ->
+ remote.getCurrentWindow().loadSettings.windowState = value
+
+ clone
+
+exports.setWindowLoadSettings = (settings) ->
+ windowLoadSettings = settings
+ location.hash = encodeURIComponent(JSON.stringify(settings))
diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee
index 378969481..fb4b19dc3 100644
--- a/src/workspace-element.coffee
+++ b/src/workspace-element.coffee
@@ -8,18 +8,11 @@ module.exports =
class WorkspaceElement extends HTMLElement
globalTextEditorStyleSheet: null
- createdCallback: ->
- @subscriptions = new CompositeDisposable
- @initializeContent()
- @observeScrollbarStyle()
- @observeTextEditorFontConfig()
-
attachedCallback: ->
@focus()
detachedCallback: ->
@subscriptions.dispose()
- @model.destroy()
initializeContent: ->
@classList.add 'workspace'
@@ -46,31 +39,42 @@ class WorkspaceElement extends HTMLElement
observeTextEditorFontConfig: ->
@updateGlobalTextEditorStyleSheet()
- @subscriptions.add atom.config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this)
- @subscriptions.add atom.config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this)
- @subscriptions.add atom.config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this)
+ @subscriptions.add @config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this)
+ @subscriptions.add @config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this)
+ @subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this)
updateGlobalTextEditorStyleSheet: ->
styleSheetSource = """
atom-text-editor {
- font-size: #{atom.config.get('editor.fontSize')}px;
- font-family: #{atom.config.get('editor.fontFamily')};
- line-height: #{atom.config.get('editor.lineHeight')};
+ font-size: #{@config.get('editor.fontSize')}px;
+ font-family: #{@config.get('editor.fontFamily')};
+ line-height: #{@config.get('editor.lineHeight')};
}
"""
- atom.styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles')
+ @styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles')
- initialize: (@model) ->
- @paneContainer = atom.views.getView(@model.paneContainer)
+ initialize: (@model, {@views, @workspace, @project, @config, @styles}) ->
+ throw new Error("Must pass a views parameter when initializing WorskpaceElements") unless @views?
+ throw new Error("Must pass a workspace parameter when initializing WorskpaceElements") unless @workspace?
+ throw new Error("Must pass a project parameter when initializing WorskpaceElements") unless @project?
+ throw new Error("Must pass a config parameter when initializing WorskpaceElements") unless @config?
+ throw new Error("Must pass a styles parameter when initializing WorskpaceElements") unless @styles?
+
+ @subscriptions = new CompositeDisposable
+ @initializeContent()
+ @observeScrollbarStyle()
+ @observeTextEditorFontConfig()
+
+ @paneContainer = @views.getView(@model.paneContainer)
@verticalAxis.appendChild(@paneContainer)
@addEventListener 'focus', @handleFocus.bind(this)
@panelContainers =
- top: atom.views.getView(@model.panelContainers.top)
- left: atom.views.getView(@model.panelContainers.left)
- right: atom.views.getView(@model.panelContainers.right)
- bottom: atom.views.getView(@model.panelContainers.bottom)
- modal: atom.views.getView(@model.panelContainers.modal)
+ top: @views.getView(@model.panelContainers.top)
+ left: @views.getView(@model.panelContainers.left)
+ right: @views.getView(@model.panelContainers.right)
+ bottom: @views.getView(@model.panelContainers.bottom)
+ modal: @views.getView(@model.panelContainers.modal)
@horizontalAxis.insertBefore(@panelContainers.left, @verticalAxis)
@horizontalAxis.appendChild(@panelContainers.right)
@@ -96,59 +100,10 @@ class WorkspaceElement extends HTMLElement
focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight()
runPackageSpecs: ->
- if activePath = atom.workspace.getActivePaneItem()?.getPath?()
- [projectPath] = atom.project.relativizePath(activePath)
+ if activePath = @workspace.getActivePaneItem()?.getPath?()
+ [projectPath] = @project.relativizePath(activePath)
else
- [projectPath] = atom.project.getPaths()
+ [projectPath] = @project.getPaths()
ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
-atom.commands.add 'atom-workspace',
- 'window:increase-font-size': -> @getModel().increaseFontSize()
- 'window:decrease-font-size': -> @getModel().decreaseFontSize()
- 'window:reset-font-size': -> @getModel().resetFontSize()
- 'application:about': -> ipc.send('command', 'application:about')
- 'application:run-all-specs': -> ipc.send('command', 'application:run-all-specs')
- 'application:show-preferences': -> ipc.send('command', 'application:show-settings')
- 'application:show-settings': -> ipc.send('command', 'application:show-settings')
- 'application:quit': -> ipc.send('command', 'application:quit')
- 'application:hide': -> ipc.send('command', 'application:hide')
- 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
- 'application:install-update': -> ipc.send('command', 'application:install-update')
- 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
- 'application:new-window': -> ipc.send('command', 'application:new-window')
- 'application:new-file': -> ipc.send('command', 'application:new-file')
- 'application:open': -> ipc.send('command', 'application:open')
- 'application:open-file': -> ipc.send('command', 'application:open-file')
- 'application:open-folder': -> ipc.send('command', 'application:open-folder')
- 'application:open-dev': -> ipc.send('command', 'application:open-dev')
- 'application:open-safe': -> ipc.send('command', 'application:open-safe')
- 'application:add-project-folder': -> atom.addProjectFolder()
- 'application:minimize': -> ipc.send('command', 'application:minimize')
- 'application:zoom': -> ipc.send('command', 'application:zoom')
- 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
- 'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
- 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
- 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
- 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
- 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
- 'application:open-license': -> @getModel().openLicense()
- 'window:run-package-specs': -> @runPackageSpecs()
- 'window:focus-next-pane': -> @getModel().activateNextPane()
- 'window:focus-previous-pane': -> @getModel().activatePreviousPane()
- 'window:focus-pane-above': -> @focusPaneViewAbove()
- 'window:focus-pane-below': -> @focusPaneViewBelow()
- 'window:focus-pane-on-left': -> @focusPaneViewOnLeft()
- 'window:focus-pane-on-right': -> @focusPaneViewOnRight()
- 'window:save-all': -> @getModel().saveAll()
- 'window:toggle-invisibles': -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles"))
- 'window:log-deprecation-warnings': -> Grim.logDeprecations()
- 'window:toggle-auto-indent': -> atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent"))
- 'pane:reopen-closed-item': -> @getModel().reopenItem()
- 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane()
- 'core:save': -> @getModel().saveActivePaneItem()
- 'core:save-as': -> @getModel().saveActivePaneItemAs()
-
-if process.platform is 'darwin'
- atom.commands.add 'atom-workspace', 'window:install-shell-commands', -> @getModel().installShellCommands()
-
module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype
diff --git a/src/workspace.coffee b/src/workspace.coffee
index bda6e9b45..80ef47c21 100644
--- a/src/workspace.coffee
+++ b/src/workspace.coffee
@@ -9,10 +9,7 @@ TextEditor = require './text-editor'
PaneContainer = require './pane-container'
Pane = require './pane'
Panel = require './panel'
-PanelElement = require './panel-element'
PanelContainer = require './panel-container'
-PanelContainerElement = require './panel-container-element'
-WorkspaceElement = require './workspace-element'
Task = require './task'
# Essential: Represents the state of the user interface for the entire window.
@@ -26,36 +23,24 @@ Task = require './task'
#
module.exports =
class Workspace extends Model
- atom.deserializers.add(this)
-
- @deserialize: (state) ->
- return unless state?
-
- for packageName in state.packagesWithActiveGrammars ? []
- atom.packages.getLoadedPackage(packageName)?.loadGrammarsSync()
-
- state.paneContainer = PaneContainer.deserialize(state.paneContainer)
- new this(state)
-
constructor: (params) ->
super
- @paneContainer = params?.paneContainer
- @fullScreen = params?.fullScreen ? false
- @destroyedItemURIs = params?.destroyedItemURIs ? []
+ {
+ @packageManager, @config, @project, @grammarRegistry, @notificationManager,
+ @clipboard, @viewRegistry, @grammarRegistry, @applicationDelegate, @assert,
+ @deserializerManager
+ } = params
@emitter = new Emitter
@openers = []
+ @destroyedItemURIs = []
- @paneContainer ?= new PaneContainer()
+ @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager})
@paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem)
- @directorySearchers = []
@defaultDirectorySearcher = new DefaultDirectorySearcher()
- atom.packages.serviceHub.consume(
- 'atom.directory-searcher',
- '^0.1.0',
- (provider) => @directorySearchers.unshift(provider))
+ @consumeServices(@packageManager)
@panelContainers =
top: new PanelContainer({location: 'top'})
@@ -64,69 +49,80 @@ class Workspace extends Model
bottom: new PanelContainer({location: 'bottom'})
modal: new PanelContainer({location: 'modal'})
+ @subscribeToEvents()
+
+ reset: (@packageManager) ->
+ @emitter.dispose()
+ @emitter = new Emitter
+
+ @paneContainer.destroy()
+ panelContainer.destroy() for panelContainer in @panelContainers
+
+ @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager})
+ @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem)
+
+ @panelContainers =
+ top: new PanelContainer({location: 'top'})
+ left: new PanelContainer({location: 'left'})
+ right: new PanelContainer({location: 'right'})
+ bottom: new PanelContainer({location: 'bottom'})
+ modal: new PanelContainer({location: 'modal'})
+
+ @originalFontSize = null
+ @openers = []
+ @destroyedItemURIs = []
+ @consumeServices(@packageManager)
+
+ subscribeToEvents: ->
@subscribeToActiveItem()
-
- @addOpener (filePath) ->
- switch filePath
- when 'atom://.atom/stylesheet'
- atom.project.open(atom.styles.getUserStyleSheetPath())
- when 'atom://.atom/keymap'
- atom.project.open(atom.keymaps.getUserKeymapPath())
- when 'atom://.atom/config'
- atom.project.open(atom.config.getUserConfigPath())
- when 'atom://.atom/init-script'
- atom.project.open(atom.getUserInitScriptPath())
-
- atom.views.addViewProvider Workspace, (model) ->
- new WorkspaceElement().initialize(model)
-
- atom.views.addViewProvider PanelContainer, (model) ->
- new PanelContainerElement().initialize(model)
-
- atom.views.addViewProvider Panel, (model) ->
- new PanelElement().initialize(model)
-
@subscribeToFontSize()
+ consumeServices: ({serviceHub}) ->
+ @directorySearchers = []
+ serviceHub.consume(
+ 'atom.directory-searcher',
+ '^0.1.0',
+ (provider) => @directorySearchers.unshift(provider))
+
# Called by the Serializable mixin during serialization.
serialize: ->
deserializer: 'Workspace'
paneContainer: @paneContainer.serialize()
- fullScreen: atom.isFullScreen()
packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars()
+ destroyedItemURIs: @destroyedItemURIs.slice()
+
+ deserialize: (state, deserializerManager) ->
+ for packageName in state.packagesWithActiveGrammars ? []
+ @packageManager.getLoadedPackage(packageName)?.loadGrammarsSync()
+ if state.destroyedItemURIs?
+ @destroyedItemURIs = state.destroyedItemURIs
+ @paneContainer.deserialize(state.paneContainer, deserializerManager)
getPackageNamesWithActiveGrammars: ->
packageNames = []
- addGrammar = ({includedGrammarScopes, packageName}={}) ->
+ addGrammar = ({includedGrammarScopes, packageName}={}) =>
return unless packageName
# Prevent cycles
return if packageNames.indexOf(packageName) isnt -1
packageNames.push(packageName)
for scopeName in includedGrammarScopes ? []
- addGrammar(atom.grammars.grammarForScopeName(scopeName))
+ addGrammar(@grammarRegistry.grammarForScopeName(scopeName))
return
editors = @getTextEditors()
addGrammar(editor.getGrammar()) for editor in editors
if editors.length > 0
- for grammar in atom.grammars.getGrammars() when grammar.injectionSelector
+ for grammar in @grammarRegistry.getGrammars() when grammar.injectionSelector
addGrammar(grammar)
_.uniq(packageNames)
- editorAdded: (editor) ->
-
- installShellCommands: ->
- CommandInstaller = require('./command-installer')
- commandInstaller = new CommandInstaller(atom.getVersion())
- commandInstaller.installShellCommandsInteractively()
-
subscribeToActiveItem: ->
@updateWindowTitle()
@updateDocumentEdited()
- atom.project.onDidChangePaths @updateWindowTitle
+ @project.onDidChangePaths @updateWindowTitle
@observeActivePaneItem (item) =>
@updateWindowTitle()
@@ -156,7 +152,7 @@ class Workspace extends Model
# open.
updateWindowTitle: =>
appName = 'Atom'
- projectPaths = atom.project?.getPaths() ? []
+ projectPaths = @project.getPaths() ? []
if item = @getActivePaneItem()
itemPath = item.getPath?()
itemTitle = item.getTitle?()
@@ -167,19 +163,19 @@ class Workspace extends Model
if item? and projectPath?
document.title = "#{itemTitle} - #{projectPath} - #{appName}"
- atom.setRepresentedFilename(itemPath ? projectPath)
+ @applicationDelegate.setRepresentedFilename(itemPath ? projectPath)
else if projectPath?
document.title = "#{projectPath} - #{appName}"
- atom.setRepresentedFilename(projectPath)
+ @applicationDelegate.setRepresentedFilename(projectPath)
else
document.title = "#{itemTitle} - #{appName}"
- atom.setRepresentedFilename("")
+ @applicationDelegate.setRepresentedFilename("")
# On OS X, fades the application window's proxy icon when the current file
# has been modified.
updateDocumentEdited: =>
modified = @getActivePaneItem()?.isModified?() ? false
- atom.setDocumentEdited(modified)
+ @applicationDelegate.setWindowDocumentEdited(modified)
###
Section: Event Subscription
@@ -209,11 +205,34 @@ class Workspace extends Model
# Essential: Invoke the given callback when the active pane item changes.
#
+ # Because observers are invoked synchronously, it's important not to perform
+ # any expensive operations via this method. Consider
+ # {::onDidStopChangingActivePaneItem} to delay operations until after changes
+ # stop occurring.
+ #
# * `callback` {Function} to be called when the active pane item changes.
# * `item` The active pane item.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeActivePaneItem: (callback) -> @paneContainer.onDidChangeActivePaneItem(callback)
+ onDidChangeActivePaneItem: (callback) ->
+ @paneContainer.onDidChangeActivePaneItem(callback)
+
+ # Essential: Invoke the given callback when the active pane item stops
+ # changing.
+ #
+ # Observers are called asynchronously 100ms after the last active pane item
+ # change. Handling changes here rather than in the synchronous
+ # {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly
+ # changing or closing tabs and ensures critical UI feedback, like changing the
+ # highlighted tab, gets priority over work that can be done asynchronously.
+ #
+ # * `callback` {Function} to be called when the active pane item stopts
+ # changing.
+ # * `item` The active pane item.
+ #
+ # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+ onDidStopChangingActivePaneItem: (callback) ->
+ @paneContainer.onDidStopChangingActivePaneItem(callback)
# Essential: Invoke the given callback with the current active pane item and
# with all future active pane items in the workspace.
@@ -363,11 +382,15 @@ class Workspace extends Model
# initially. Defaults to `0`.
# * `initialColumn` A {Number} indicating which column to move the cursor to
# initially. Defaults to `0`.
- # * `split` Either 'left' or 'right'. If 'left', the item will be opened in
- # leftmost pane of the current active pane's row. If 'right', the
- # item will be opened in the rightmost pane of the current active pane's row.
+ # * `split` Either 'left', 'right', 'top' or 'bottom'.
+ # If 'left', the item will be opened in leftmost pane of the current active pane's row.
+ # If 'right', the item will be opened in the rightmost pane of the current active pane's row.
+ # If 'up', the item will be opened in topmost pane of the current active pane's row.
+ # If 'down', the item will be opened in the bottommost pane of the current active pane's row.
# * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
# containing pane. Defaults to `true`.
+ # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
+ # on containing pane. Defaults to `true`.
# * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
# activate an existing item for the given URI on any pane.
# If `false`, only the active pane will be searched for
@@ -377,7 +400,7 @@ class Workspace extends Model
open: (uri, options={}) ->
searchAllPanes = options.searchAllPanes
split = options.split
- uri = atom.project.resolvePath(uri)
+ uri = @project.resolvePath(uri)
pane = @paneContainer.paneForURI(uri) if searchAllPanes
pane ?= switch split
@@ -385,6 +408,10 @@ class Workspace extends Model
@getActivePane().findLeftmostSibling()
when 'right'
@getActivePane().findOrCreateRightmostSibling()
+ when 'up'
+ @getActivePane().findTopmostSibling()
+ when 'down'
+ @getActivePane().findOrCreateBottommostSibling()
else
@getActivePane()
@@ -406,50 +433,51 @@ class Workspace extends Model
# initially. Defaults to `0`.
# * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
# the containing pane. Defaults to `true`.
+ # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
+ # on containing pane. Defaults to `true`.
openSync: (uri='', options={}) ->
{initialLine, initialColumn} = options
activatePane = options.activatePane ? true
+ activateItem = options.activateItem ? true
- uri = atom.project.resolvePath(uri)
+ uri = @project.resolvePath(uri)
item = @getActivePane().itemForURI(uri)
if uri
item ?= opener(uri, options) for opener in @getOpeners() when not item
- item ?= atom.project.openSync(uri, {initialLine, initialColumn})
+ item ?= @project.openSync(uri, {initialLine, initialColumn})
- @getActivePane().activateItem(item)
+ @getActivePane().activateItem(item) if activateItem
@itemOpened(item)
@getActivePane().activate() if activatePane
item
openURIInPane: (uri, pane, options={}) ->
activatePane = options.activatePane ? true
+ activateItem = options.activateItem ? true
if uri?
item = pane.itemForURI(uri)
item ?= opener(uri, options) for opener in @getOpeners() when not item
try
- item ?= atom.project.open(uri, options)
+ item ?= @openTextFile(uri, options)
catch error
switch error.code
when 'CANCELLED'
return Promise.resolve()
when 'EACCES'
- atom.notifications.addWarning("Permission denied '#{error.path}'")
+ @notificationManager.addWarning("Permission denied '#{error.path}'")
return Promise.resolve()
when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL'
- atom.notifications.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message)
+ @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message)
return Promise.resolve()
else
throw error
Promise.resolve(item)
.then (item) =>
- if not pane
- pane = new Pane(items: [item])
- @paneContainer.root = pane
@itemOpened(item)
- pane.activateItem(item)
+ pane.activateItem(item) if activateItem
pane.activate() if activatePane
initialLine = initialColumn = 0
@@ -464,6 +492,42 @@ class Workspace extends Model
@emitter.emit 'did-open', {uri, pane, item, index}
item
+ openTextFile: (uri, options) ->
+ filePath = @project.resolvePath(uri)
+
+ if filePath?
+ try
+ fs.closeSync(fs.openSync(filePath, 'r'))
+ catch error
+ # allow ENOENT errors to create an editor for paths that dont exist
+ throw error unless error.code is 'ENOENT'
+
+ fileSize = fs.getSizeSync(filePath)
+
+ largeFileMode = fileSize >= 2 * 1048576 # 2MB
+ if fileSize >= 20 * 1048576 # 20MB
+ choice = @applicationDelegate.confirm
+ message: 'Atom will be unresponsive during the loading of very large files.'
+ detailedMessage: "Do you still want to load this file?"
+ buttons: ["Proceed", "Cancel"]
+ if choice is 1
+ error = new Error
+ error.code = 'CANCELLED'
+ throw error
+
+ @project.bufferForPath(filePath, options).then (buffer) =>
+ @buildTextEditor(_.extend({buffer, largeFileMode}, options))
+
+ # Extended: Create a new text editor.
+ #
+ # Returns a {TextEditor}.
+ buildTextEditor: (params) ->
+ params = _.extend({
+ @config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
+ @grammarRegistry, @project, @assert, @applicationDelegate
+ }, params)
+ new TextEditor(params)
+
# Public: Asynchronously reopens the last-closed item's URI if it hasn't already been
# reopened.
#
@@ -617,20 +681,20 @@ class Workspace extends Model
# Increase the editor font size by 1px.
increaseFontSize: ->
- atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1)
+ @config.set("editor.fontSize", @config.get("editor.fontSize") + 1)
# Decrease the editor font size by 1px.
decreaseFontSize: ->
- fontSize = atom.config.get("editor.fontSize")
- atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1
+ fontSize = @config.get("editor.fontSize")
+ @config.set("editor.fontSize", fontSize - 1) if fontSize > 1
# Restore to the window's original editor font size.
resetFontSize: ->
if @originalFontSize
- atom.config.set("editor.fontSize", @originalFontSize)
+ @config.set("editor.fontSize", @originalFontSize)
subscribeToFontSize: ->
- atom.config.onDidChange 'editor.fontSize', ({oldValue}) =>
+ @config.onDidChange 'editor.fontSize', ({oldValue}) =>
@originalFontSize ?= oldValue
# Removes the item's uri from the list of potential items to reopen.
@@ -789,13 +853,14 @@ class Workspace extends Model
Section: Searching and Replacing
###
- # Public: Performs a search across all the files in the workspace.
+ # Public: Performs a search across all files in the workspace.
#
# * `regex` {RegExp} to search with.
- # * `options` (optional) {Object} (default: {})
- # * `paths` An {Array} of glob patterns to search within
- # * `onPathsSearched` (optional) {Function}
- # * `iterator` {Function} callback on each file found
+ # * `options` (optional) {Object}
+ # * `paths` An {Array} of glob patterns to search within.
+ # * `onPathsSearched` (optional) {Function} to be periodically called
+ # with number of paths searched.
+ # * `iterator` {Function} callback on each file found.
#
# Returns a `Promise` with a `cancel()` method that will cancel all
# of the underlying searches that were started as part of this scan.
@@ -807,7 +872,7 @@ class Workspace extends Model
# Find a searcher for every Directory in the project. Each searcher that is matched
# will be associated with an Array of Directory objects in the Map.
directoriesForSearcher = new Map()
- for directory in atom.project.getDirectories()
+ for directory in @project.getDirectories()
searcher = @defaultDirectorySearcher
for directorySearcher in @directorySearchers
if directorySearcher.canSearchDirectory(directory)
@@ -838,15 +903,15 @@ class Workspace extends Model
# Kick off all of the searches and unify them into one Promise.
allSearches = []
- directoriesForSearcher.forEach (directories, searcher) ->
+ directoriesForSearcher.forEach (directories, searcher) =>
searchOptions =
inclusions: options.paths or []
includeHidden: true
- excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths')
- exclusions: atom.config.get('core.ignoredNames')
- follow: atom.config.get('core.followSymlinks')
- didMatch: (result) ->
- iterator(result) unless atom.project.isPathModified(result.filePath)
+ excludeVcsIgnores: @config.get('core.excludeVcsIgnoredPaths')
+ exclusions: @config.get('core.ignoredNames')
+ follow: @config.get('core.followSymlinks')
+ didMatch: (result) =>
+ iterator(result) unless @project.isPathModified(result.filePath)
didError: (error) ->
iterator(null, error)
didSearchPaths: (count) -> onPathsSearched(searcher, count)
@@ -854,9 +919,9 @@ class Workspace extends Model
allSearches.push(directorySearcher)
searchPromise = Promise.all(allSearches)
- for buffer in atom.project.getBuffers() when buffer.isModified()
+ for buffer in @project.getBuffers() when buffer.isModified()
filePath = buffer.getPath()
- continue unless atom.project.contains(filePath)
+ continue unless @project.contains(filePath)
matches = []
buffer.scan regex, (match) -> matches.push match
iterator {filePath, matches} if matches.length > 0
@@ -895,15 +960,15 @@ class Workspace extends Model
# Public: Performs a replace across all the specified files in the project.
#
# * `regex` A {RegExp} to search with.
- # * `replacementText` Text to replace all matches of regex with
- # * `filePaths` List of file path strings to run the replace on.
+ # * `replacementText` {String} to replace all matches of regex with.
+ # * `filePaths` An {Array} of file path strings to run the replace on.
# * `iterator` A {Function} callback on each file with replacements:
- # * `options` {Object} with keys `filePath` and `replacements`
+ # * `options` {Object} with keys `filePath` and `replacements`.
#
# Returns a `Promise`.
replace: (regex, replacementText, filePaths, iterator) ->
- new Promise (resolve, reject) ->
- openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
+ new Promise (resolve, reject) =>
+ openPaths = (buffer.getPath() for buffer in @project.getBuffers())
outOfProcessPaths = _.difference(filePaths, openPaths)
inProcessFinished = not openPaths.length
@@ -922,7 +987,7 @@ class Workspace extends Model
task.on 'replace:path-replaced', iterator
task.on 'replace:file-error', (error) -> iterator(null, error)
- for buffer in atom.project.getBuffers()
+ for buffer in @project.getBuffers()
continue unless buffer.getPath() in filePaths
replacements = buffer.replace(regex, replacementText, iterator)
iterator({filePath: buffer.getPath(), replacements}) if replacements
diff --git a/static/index.js b/static/index.js
index 11527e963..927c25266 100644
--- a/static/index.js
+++ b/static/index.js
@@ -76,7 +76,7 @@
setupVmCompatibility()
setupCsonCache(CompileCache.getCacheDirectory())
- require(loadSettings.bootstrapScript)
+ require(loadSettings.windowInitializationScript)
require('ipc').sendChannel('window-command', 'window:loaded')
}
@@ -141,7 +141,7 @@
} else {
currentWindow.openDevTools()
currentWindow.once('devtools-opened', function () {
- setTimeout(profile, 100)
+ setTimeout(profile, 1000)
})
}
}
@@ -168,7 +168,7 @@
var backgroundStylesheet = document.createElement('style')
backgroundStylesheet.type = 'text/css'
- backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }'
+ backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }'
document.head.appendChild(backgroundStylesheet)
// Remove once the page loads