diff --git a/apm/package.json b/apm/package.json index 84f73451d..e7a0c8fdc 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.157.0" + "atom-package-manager": "0.158.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 071dd1295..7c687ac92 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -223,7 +223,7 @@ module.exports = (grunt) -> ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build'] ciTasks.push('dump-symbols') if process.platform isnt 'win32' - ciTasks.push('set-version', 'check-licenses', 'lint') + ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar') ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('create-windows-installer') if process.platform is 'win32' ciTasks.push('test') if process.platform is 'darwin' @@ -231,6 +231,6 @@ module.exports = (grunt) -> ciTasks.push('publish-build') unless process.env.TRAVIS grunt.registerTask('ci', ciTasks) - defaultTasks = ['download-atom-shell', 'download-atom-shell-chromedriver', 'build', 'set-version'] + defaultTasks = ['download-atom-shell', 'download-atom-shell-chromedriver', 'build', 'set-version', 'generate-asar'] defaultTasks.push 'install' unless process.platform is 'linux' grunt.registerTask('default', defaultTasks) diff --git a/build/package.json b/build/package.json index 212f9c3ed..3f0a3caef 100644 --- a/build/package.json +++ b/build/package.json @@ -6,13 +6,14 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { + "asar": "^0.4.4", "async": "~0.2.9", "donna": "1.0.10", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", "grunt": "~0.4.1", - "grunt-atom-shell-installer": "^0.28.0", + "grunt-atom-shell-installer": "^0.29.0", "grunt-cli": "~0.1.9", "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe", "grunt-contrib-coffee": "~0.12.0", @@ -31,7 +32,7 @@ "request": "~2.27.0", "rimraf": "~2.2.2", "runas": "^2", - "tello": "1.0.4", + "tello": "1.0.5", "temp": "~0.8.1", "underscore-plus": "1.x", "unzip": "~0.1.9", diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index a94b38f75..3411f4dae 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -22,7 +22,7 @@ module.exports = (grunt) -> mkdir appDir if process.platform isnt 'win32' - cp 'atom.sh', path.join(appDir, 'atom.sh') + cp 'atom.sh', path.resolve(appDir, '..', 'new-app', 'atom.sh') cp 'package.json', path.join(appDir, 'package.json') @@ -65,6 +65,7 @@ module.exports = (grunt) -> path.join('jasmine-node', 'node_modules', 'gaze') path.join('jasmine-node', 'spec') path.join('node_modules', 'nan') + path.join('node_modules', 'native-mate') path.join('build', 'binding.Makefile') path.join('build', 'config.gypi') path.join('build', 'gyp-mac-tool') @@ -143,13 +144,13 @@ module.exports = (grunt) -> for directory in packageDirectories cp directory, path.join(appDir, directory), filter: filterPackage - cp 'spec', path.join(appDir, 'spec') + cp 'spec', path.join(appDir, 'spec'), filter: /fixtures|integration|.+-spec\.coffee/ cp 'src', path.join(appDir, 'src'), filter: /.+\.(cson|coffee)$/ cp 'static', path.join(appDir, 'static') - cp path.join('apm', 'node_modules', 'atom-package-manager'), path.join(appDir, 'apm'), filter: filterNodeModule + cp path.join('apm', 'node_modules', 'atom-package-manager'), path.resolve(appDir, '..', 'new-app', 'apm'), filter: filterNodeModule if process.platform isnt 'win32' - fs.symlinkSync(path.join('..', '..', 'bin', 'apm'), path.join(appDir, 'apm', 'node_modules', '.bin', 'apm')) + fs.symlinkSync(path.join('..', '..', 'bin', 'apm'), path.resolve(appDir, '..', 'new-app', 'apm', 'node_modules', '.bin', 'apm')) if process.platform is 'darwin' grunt.file.recurse path.join('resources', 'mac'), (sourcePath, rootDirectory, subDirectory='', filename) -> diff --git a/build/tasks/generate-asar-task.coffee b/build/tasks/generate-asar-task.coffee new file mode 100644 index 000000000..a71d88dfe --- /dev/null +++ b/build/tasks/generate-asar-task.coffee @@ -0,0 +1,35 @@ +asar = require 'asar' +fs = require 'fs' +path = require 'path' + +module.exports = (grunt) -> + {cp, rm} = require('./task-helpers')(grunt) + + grunt.registerTask 'generate-asar', 'Generate asar archive for the app', -> + done = @async() + + unpack = [ + '*.node' + '.ctags' + 'ctags-darwin' + 'ctags-linux' + 'ctags-win32.exe' + ] + unpack = "{#{unpack.join(',')}}" + + appDir = grunt.config.get('atom.appDir') + unless fs.existsSync(appDir) + grunt.log.error 'The app has to be built before generating asar archive.' + return done(false) + + asar.createPackageWithOptions appDir, path.resolve(appDir, '..', 'app.asar'), {unpack}, (err) -> + return done(err) if err? + + rm appDir + fs.renameSync path.resolve(appDir, '..', 'new-app'), appDir + + ctagsFolder = path.join("#{appDir}.asar.unpacked", 'node_modules', 'symbols-view', 'vendor') + for ctagsFile in fs.readdirSync(ctagsFolder) + fs.chmodSync(path.join(ctagsFolder, ctagsFile), "755") + + done() diff --git a/build/tasks/generate-license-task.coffee b/build/tasks/generate-license-task.coffee index 6b616f5cb..eaf1a9a66 100644 --- a/build/tasks/generate-license-task.coffee +++ b/build/tasks/generate-license-task.coffee @@ -17,7 +17,7 @@ module.exports = (grunt) -> licenseText = getLicenseText(dependencyLicenses) if mode is 'save' - targetPath = path.join(grunt.config.get('atom.appDir'), 'LICENSE.md') + targetPath = path.resolve(grunt.config.get('atom.appDir'), '..', 'LICENSE.md') fs.writeFileSync(targetPath, licenseText) else console.log licenseText diff --git a/package.json b/package.json index 8fd36d5bd..744994a8f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.192.0", + "version": "0.193.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -113,7 +113,7 @@ "settings-view": "0.192.0", "snippets": "0.88.0", "spell-check": "0.55.0", - "status-bar": "0.68.0", + "status-bar": "0.69.0", "styleguide": "0.44.0", "symbols-view": "0.94.0", "tabs": "0.67.0", @@ -128,9 +128,9 @@ "language-coffee-script": "0.39.0", "language-csharp": "0.5.0", "language-css": "0.28.0", - "language-gfm": "0.68.0", + "language-gfm": "0.69.0", "language-git": "0.10.0", - "language-go": "0.23.0", + "language-go": "0.25.0", "language-html": "0.32.0", "language-hyperlink": "0.12.2", "language-java": "0.14.0", diff --git a/script/mkdeb b/script/mkdeb index 272fe23f3..c39b6d649 100755 --- a/script/mkdeb +++ b/script/mkdeb @@ -33,7 +33,7 @@ cp "$ICON_FILE" "$TARGET/usr/share/pixmaps" # Copy generated LICENSE.md to /usr/share/doc/atom/copyright mkdir -m $FILE_MODE -p "$TARGET/usr/share/doc/atom" -cp "$TARGET/usr/share/atom/resources/app/LICENSE.md" "$TARGET/usr/share/doc/atom/copyright" +cp "$TARGET/usr/share/atom/resources/LICENSE.md" "$TARGET/usr/share/doc/atom/copyright" # Add lintian overrides mkdir -m $FILE_MODE -p "$TARGET/usr/share/lintian/overrides" diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index a293edccd..99a7f432e 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -139,7 +139,7 @@ describe "the `atom` global", -> windowState: null spyOn(Atom, 'getLoadSettings').andCallFake -> loadSettings - spyOn(Atom, 'getStorageDirPath').andReturn(temp.mkdirSync("storage-dir-")) + spyOn(Atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-")) atom.mode = "editor" atom.state.stuff = "cool" diff --git a/spec/fixtures/packages/theme-with-invalid-styles/index.less b/spec/fixtures/packages/theme-with-invalid-styles/index.less new file mode 100644 index 000000000..b554d9e46 --- /dev/null +++ b/spec/fixtures/packages/theme-with-invalid-styles/index.less @@ -0,0 +1 @@ +<> diff --git a/spec/fixtures/packages/theme-with-invalid-styles/package.json b/spec/fixtures/packages/theme-with-invalid-styles/package.json new file mode 100644 index 000000000..1dd1ee48b --- /dev/null +++ b/spec/fixtures/packages/theme-with-invalid-styles/package.json @@ -0,0 +1,4 @@ +{ + "name": "theme-with-invalid-styles", + "theme": "ui" +} diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 08d3659c3..0de6644b3 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -108,6 +108,15 @@ buildAtomClient = (args, env) -> ]), env: extend({}, process.env, env)) done() + .addCommand "dispatchCommand", (command, done) -> + @execute "atom.commands.dispatch(document.activeElement, '#{command}')" + .call(done) + + .addCommand "simulateQuit", (done) -> + @execute -> atom.unloadEditorWindow() + .execute -> require("remote").require("app").emit("before-quit") + .call(done) + module.exports = (args, env, fn) -> [chromedriver, chromedriverLogs, chromedriverExit] = [] @@ -133,6 +142,7 @@ module.exports = (args, env, fn) -> waitsFor("webdriver to finish", (done) -> finish = once -> client + .simulateQuit() .end() .then(-> chromedriver.kill()) .then(chromedriverExit.then( @@ -147,7 +157,7 @@ module.exports = (args, env, fn) -> client = buildAtomClient(args, env) client.on "error", (err) -> - jasmine.getEnv().currentSpec.fail(JSON.stringify(err)) + jasmine.getEnv().currentSpec.fail(new Error(err.response?.body?.value?.message)) finish() fn(client.init()).then(finish) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index e0df3ad9b..2dcf6ba38 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -6,22 +6,22 @@ return unless process.env.ATOM_INTEGRATION_TESTS_ENABLED fs = require "fs" path = require "path" temp = require("temp").track() -AtomHome = temp.mkdirSync('atom-home') -fs.writeFileSync(path.join(AtomHome, 'config.cson'), fs.readFileSync(path.join(__dirname, 'fixtures', 'atom-home', 'config.cson'))) runAtom = require("./helpers/start-atom") describe "Starting Atom", -> - [tempDirPath, otherTempDirPath] = [] + [tempDirPath, otherTempDirPath, atomHome] = [] beforeEach -> jasmine.useRealClock() + atomHome = temp.mkdirSync('atom-home') + fs.writeFileSync(path.join(atomHome, 'config.cson'), fs.readFileSync(path.join(__dirname, 'fixtures', 'atom-home', 'config.cson'))) tempDirPath = temp.mkdirSync("empty-dir") otherTempDirPath = temp.mkdirSync("another-temp-dir") describe "opening a new file", -> it "opens the parent directory and creates an empty text editor", -> - runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: AtomHome}, (client) -> + runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> client .waitForWindowCount(1, 1000) .waitForExist("atom-workspace", 5000) @@ -36,13 +36,14 @@ describe "Starting Atom", -> .keys("Hello!") .execute -> atom.workspace.getActiveTextEditor().getText() .then ({value}) -> expect(value).toBe "Hello!" + .dispatchCommand("editor:delete-line") describe "when there is already a window open", -> it "reuses that window when opening files, but not when opening directories", -> tempFilePath = path.join(temp.mkdirSync("a-third-dir"), "a-file") fs.writeFileSync(tempFilePath, "This file was already here.") - runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: AtomHome}, (client) -> + runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> client .waitForWindowCount(1, 1000) .waitForExist("atom-workspace", 5000) @@ -50,7 +51,7 @@ describe "Starting Atom", -> # Opening another file reuses the same window and does not change the # project paths. - .startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome) + .startAnotherAtom([tempFilePath], ATOM_HOME: atomHome) .waitForPaneItemCount(2, 5000) .waitForWindowCount(1, 1000) .treeViewRootDirectories() @@ -60,7 +61,7 @@ describe "Starting Atom", -> # Opening another directory creates a second window. .waitForNewWindow(-> - @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) + @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome) , 5000) .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(0, 1000) @@ -69,15 +70,14 @@ describe "Starting Atom", -> describe "reopening a directory that was previously opened", -> it "remembers the state of the window", -> - runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> client .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(0, 3000) .execute -> atom.workspace.open() .waitForPaneItemCount(1, 3000) - .execute -> atom.unloadEditorWindow() - runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> client .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 5000) @@ -87,7 +87,7 @@ describe "Starting Atom", -> nestedDir = path.join(otherTempDirPath, "nested-dir") fs.mkdirSync(nestedDir) - runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: atomHome}, (client) -> client .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() @@ -95,42 +95,67 @@ describe "Starting Atom", -> # Opening one of those directories again reuses the same window and # does not change the project paths. - .startAnotherAtom([nestedDir], ATOM_HOME: AtomHome) + .startAnotherAtom([nestedDir], ATOM_HOME: atomHome) .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath]) describe "when there is an existing window with no project path", -> - describe "opening a directory", -> - it "opens the directory in the existing window", -> - runAtom [], {ATOM_HOME: AtomHome}, (client) -> - client - .waitForExist("atom-workspace") + it "reuses that window to open a directory", -> + runAtom [], {ATOM_HOME: atomHome}, (client) -> + client + .waitForExist("atom-workspace") + .treeViewRootDirectories() + .then ({value}) -> expect(value).toEqual([]) + + .startAnotherAtom([tempDirPath], ATOM_HOME: atomHome) + .waitUntil(-> + @treeViewRootDirectories() + .then ({value}) -> value[0] is tempDirPath + , 5000) + .then (result) -> expect(result).toBe(true) + .waitForWindowCount(1, 5000) + + describe "launching with no path", -> + it "opens a new window with a single untitled buffer", -> + runAtom [], {ATOM_HOME: atomHome}, (client) -> + client + .waitForExist("atom-workspace") + .waitForPaneItemCount(1, 5000) + + # Opening with no file paths always creates a new window, even if + # existing windows have no project paths. + .waitForNewWindow(-> + @startAnotherAtom([], ATOM_HOME: atomHome) + , 5000) + .waitForExist("atom-workspace") + .waitForPaneItemCount(1, 5000) + + it "reopens any previously opened windows", -> + runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> + client + .waitForExist("atom-workspace") + .waitForNewWindow(-> + @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome) + , 5000) + .waitForExist("atom-workspace") + + runAtom [], {ATOM_HOME: atomHome}, (client) -> + windowProjectPaths = [] + + client + .waitForWindowCount(2, 10000) + .then ({value: windowHandles}) -> + @window(windowHandles[0]) .treeViewRootDirectories() - .then ({value}) -> expect(value).toEqual([]) + .then ({value: directories}) -> windowProjectPaths.push(directories) - .startAnotherAtom([tempDirPath], ATOM_HOME: AtomHome) - .waitUntil(-> - @treeViewRootDirectories() - .then ({value}) -> value[0] is tempDirPath - , 5000) - .then (result) -> expect(result).toBe(true) - .waitForWindowCount(1, 5000) + .window(windowHandles[1]) + .treeViewRootDirectories() + .then ({value: directories}) -> windowProjectPaths.push(directories) - describe "launching with no path", -> - it "always opens a new window with a single untitled buffer", -> - runAtom [], {ATOM_HOME: AtomHome}, (client) -> - client - .waitForExist("atom-workspace") - .waitForPaneItemCount(1, 5000) - - runAtom [], {ATOM_HOME: AtomHome}, (client) -> - client - .waitForExist("atom-workspace") - .waitForPaneItemCount(1, 5000) - - # Opening with no file paths always creates a new window, even if - # existing windows have no project paths. - .waitForNewWindow(-> - @startAnotherAtom([], ATOM_HOME: AtomHome) - , 5000) + .call -> + expect(windowProjectPaths.sort()).toEqual [ + [tempDirPath] + [otherTempDirPath] + ].sort() diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index db2a76135..8dcc6bc37 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -28,6 +28,12 @@ describe "PackageManager", -> expect(pack.metadata.name).toBe "package-with-invalid-styles" expect(pack.stylesheets.length).toBe 0 + addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(-> pack.reloadStylesheets()).not.toThrow() + expect(addErrorHandler.callCount).toBe 2 + expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to reload the package-with-invalid-styles package stylesheets") + it "returns null if the package has an invalid package.json", -> addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee new file mode 100644 index 000000000..0a5b11b39 --- /dev/null +++ b/spec/pane-container-element-spec.coffee @@ -0,0 +1,134 @@ +PaneContainer = require '../src/pane-container' +PaneAxisElement = require '../src/pane-axis-element' +PaneAxis = require '../src/pane-axis' + +describe "PaneContainerElement", -> + describe "when panes are added or removed", -> + [paneAxisElement, paneAxis] = [] + + beforeEach -> + paneAxis = new PaneAxis + paneAxisElement = new PaneAxisElement().initialize(paneAxis) + + childTagNames = -> + child.nodeName.toLowerCase() for child in paneAxisElement.children + + it "inserts or removes resize elements", -> + expect(childTagNames()).toEqual [] + + paneAxis.addChild(new PaneAxis) + expect(childTagNames()).toEqual [ + 'atom-pane-axis' + ] + + paneAxis.addChild(new PaneAxis) + expect(childTagNames()).toEqual [ + 'atom-pane-axis' + 'atom-pane-resize-handle' + 'atom-pane-axis' + ] + + paneAxis.addChild(new PaneAxis) + expect(childTagNames()).toEqual [ + 'atom-pane-axis' + 'atom-pane-resize-handle' + 'atom-pane-axis' + 'atom-pane-resize-handle' + 'atom-pane-axis' + ] + + paneAxis.removeChild(paneAxis.getChildren()[2]) + expect(childTagNames()).toEqual [ + 'atom-pane-axis' + 'atom-pane-resize-handle' + 'atom-pane-axis' + ] + + describe "when the resize element is dragged ", -> + [container, containerElement] = [] + + beforeEach -> + container = new PaneContainer + containerElement = atom.views.getView(container) + document.querySelector('#jasmine-content').appendChild(containerElement) + + dragElementToPosition = (element, clientX) -> + element.dispatchEvent(new MouseEvent('mousedown', + view: window + bubbles: true + button: 0 + )) + + element.dispatchEvent(new MouseEvent 'mousemove', + view: window + bubbles: true + clientX: clientX + ) + + element.dispatchEvent(new MouseEvent 'mouseup', + iew: window + bubbles: true + button: 0 + ) + + getElementWidth = (element) -> + element.getBoundingClientRect().width + + expectPaneScale = (pairs...) -> + for [pane, expectedFlexScale] in pairs + expect(pane.getFlexScale()).toBeCloseTo(expectedFlexScale, 0.1) + + getResizeElement = (i) -> + containerElement.querySelectorAll('atom-pane-resize-handle')[i] + + getPaneElement = (i) -> + containerElement.querySelectorAll('atom-pane')[i] + + it "adds and removes panes in the direction that the pane is being dragged", -> + leftPane = container.getActivePane() + expectPaneScale [leftPane, 1] + + middlePane = leftPane.splitRight() + expectPaneScale [leftPane, 1], [middlePane, 1] + + dragElementToPosition( + getResizeElement(0), + getElementWidth(getPaneElement(0)) / 2 + ) + expectPaneScale [leftPane, 0.5], [middlePane, 1.5] + + rightPane = middlePane.splitRight() + expectPaneScale [leftPane, 0.5], [middlePane, 1.5], [rightPane, 1] + + dragElementToPosition( + getResizeElement(1), + getElementWidth(getPaneElement(0)) + getElementWidth(getPaneElement(1)) / 2 + ) + expectPaneScale [leftPane, 0.5], [middlePane, 0.75], [rightPane, 1.75] + + middlePane.close() + expectPaneScale [leftPane, 0.44], [rightPane, 1.55] + + leftPane.close() + expectPaneScale [rightPane, 1] + + it "splits or closes panes in orthogonal direction that the pane is being dragged", -> + leftPane = container.getActivePane() + expectPaneScale [leftPane, 1] + + rightPane = leftPane.splitRight() + expectPaneScale [leftPane, 1], [rightPane, 1] + + dragElementToPosition( + getResizeElement(0), + getElementWidth(getPaneElement(0)) / 2 + ) + expectPaneScale [leftPane, 0.5], [rightPane, 1.5] + + # dynamically split pane, pane's flexScale will become to 1 + lowerPane = leftPane.splitDown() + expectPaneScale [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5] + + # dynamically close pane, the pane's flexscale will recorver to origin value + lowerPane.close() + expectPaneScale [leftPane, 0.5], [rightPane, 1.5] diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index ac6e1d26f..7d6410048 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -696,7 +696,10 @@ describe "Pane", -> pane = null beforeEach -> - pane = new Pane(items: [new Item("A", "a"), new Item("B", "b"), new Item("C", "c")]) + params = + items: [new Item("A", "a"), new Item("B", "b"), new Item("C", "c")] + flexScale: 2 + pane = new Pane(params) it "can serialize and deserialize the pane and all its items", -> newPane = pane.testSerialization() diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 2df5aa68c..19fa247e0 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -118,6 +118,33 @@ describe "PaneView", -> paneModel.activateItem(view2) expect(pane.itemViews.find('#view-2').length).toBe 1 + describe "when the new activeItem implements ::getPath", -> + beforeEach -> + paneModel.activateItem(editor1) + + it "adds the file path as a data attribute to the pane", -> + expect(pane).toHaveAttr('data-active-item-path') + + it "adds the file name as a data attribute to the pane", -> + expect(pane).toHaveAttr('data-active-item-name') + + describe "when the activeItem is destroyed", -> + it "removes the data attributes", -> + pane.destroyItems() + expect(pane).not.toHaveAttr('data-active-item-path') + expect(pane).not.toHaveAttr('data-active-item-name') + + describe "when the new activeItem does not implement ::getPath", -> + beforeEach -> + paneModel.activateItem(editor1) + paneModel.activateItem(document.createElement('div')) + + it "does not add the file path as a data attribute to the pane", -> + expect(pane).not.toHaveAttr('data-active-item-path') + + it "does not add the file name as data attribute to the pane", -> + expect(pane).not.toHaveAttr('data-active-item-name') + describe "when an item is destroyed", -> it "triggers the 'pane:item-removed' event with the item and its former index", -> itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") @@ -333,3 +360,30 @@ describe "PaneView", -> pane3 = container3.getRoot() container3.attachToDom() expect(pane3).not.toMatchSelector(':has(:focus)') + + describe "drag and drop", -> + buildDragEvent = (type, files) -> + dataTransfer = + files: files + data: {} + setData: (key, value) -> @data[key] = value + getData: (key) -> @data[key] + + event = new CustomEvent("drop") + event.dataTransfer = dataTransfer + event + + describe "when a file is dragged to window", -> + it "opens it", -> + spyOn(atom, "open") + event = buildDragEvent("drop", [ {path: "/fake1"}, {path: "/fake2"} ]) + pane[0].dispatchEvent(event) + expect(atom.open.callCount).toBe 1 + expect(atom.open.argsForCall[0][0]).toEqual pathsToOpen: ['/fake1', '/fake2'] + + describe "when a non-file is dragged to window", -> + it "does nothing", -> + spyOn(atom, "open") + event = buildDragEvent("drop", []) + pane[0].dispatchEvent(event) + expect(atom.open).not.toHaveBeenCalled() diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index d5d18b2ba..a0905ddf7 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -163,7 +163,6 @@ afterEach -> jasmine.unspy(atom, 'saveSync') ensureNoPathSubscriptions() - atom.grammars.clearObservers() waits(0) # yield to ui thread to make screen update more frequently ensureNoPathSubscriptions = -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index c39e4994b..98a85f389 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -390,6 +390,13 @@ describe "ThemeManager", -> expect(note.getType()).toBe 'error' expect(note.getMessage()).toContain 'Unable to watch path' + it "adds a notification when a theme's stylesheet is invalid", -> + addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(-> atom.packages.activatePackage('theme-with-invalid-styles')).not.toThrow() + expect(addErrorHandler.callCount).toBe 2 + expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to activate the theme-with-invalid-styles theme") + describe "when a non-existent theme is present in the config", -> beforeEach -> spyOn(console, 'warn') diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 3c1a5b4ae..376a71ab3 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -141,33 +141,6 @@ describe "Window", -> expect(buffer.getSubscriptionCount()).toBe 0 - describe "drag and drop", -> - buildDragEvent = (type, files) -> - dataTransfer = - files: files - data: {} - setData: (key, value) -> @data[key] = value - getData: (key) -> @data[key] - - event = new CustomEvent("drop") - event.dataTransfer = dataTransfer - event - - describe "when a file is dragged to window", -> - it "opens it", -> - spyOn(atom, "open") - event = buildDragEvent("drop", [ {path: "/fake1"}, {path: "/fake2"} ]) - document.dispatchEvent(event) - expect(atom.open.callCount).toBe 1 - expect(atom.open.argsForCall[0][0]).toEqual pathsToOpen: ['/fake1', '/fake2'] - - describe "when a non-file is dragged to window", -> - it "does nothing", -> - spyOn(atom, "open") - event = buildDragEvent("drop", []) - document.dispatchEvent(event) - expect(atom.open).not.toHaveBeenCalled() - describe "when a link is clicked", -> it "opens the http/https links in an external application", -> shell = require 'shell' diff --git a/src/atom.coffee b/src/atom.coffee index 595dc7930..e2544640d 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -14,6 +14,7 @@ Model = require './model' {$} = require './space-pen-extensions' WindowEventHandler = require './window-event-handler' StylesElement = require './styles-element' +StorageFolder = require './storage-folder' # Essential: Atom global for dealing with packages, themes, menus, and the window. # @@ -73,34 +74,24 @@ class Atom extends Model # Loads and returns the serialized state corresponding to this window # if it exists; otherwise returns undefined. @loadState: (mode) -> - statePath = @getStatePath(@getLoadSettings().initialPaths, mode) + if stateKey = @getStateKey(@getLoadSettings().initialPaths, mode) + if state = @getStorageFolder().load(stateKey) + return state - if fs.existsSync(statePath) + if windowState = @getLoadSettings().windowState try - stateString = fs.readFileSync(statePath, 'utf8') + JSON.parse(@getLoadSettings().windowState) catch error - console.warn "Error reading window state: #{statePath}", error.stack, error - else - stateString = @getLoadSettings().windowState - - try - JSON.parse(stateString) if stateString? - catch error - console.warn "Error parsing window state: #{statePath} #{error.stack}", 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. - @getStatePath: (paths, mode) -> - switch mode - when 'spec' - filename = 'spec' - when 'editor' - if paths?.length > 0 - sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') - filename = "editor-#{sha1}" - - if filename - path.join(@getStorageDirPath(), filename) + @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 @@ -110,11 +101,8 @@ class Atom extends Model @getConfigDirPath: -> @configDirPath ?= process.env.ATOM_HOME - # Get the path to Atom's storage directory. - # - # Returns the absolute path to ~/.atom/storage - @getStorageDirPath: -> - @storageDirPath ?= path.join(@getConfigDirPath(), 'storage') + @getStorageFolder: -> + @storageFolder ?= new StorageFolder(@getConfigDirPath()) # Returns the load settings hash associated with the current window. @getLoadSettings: -> @@ -227,7 +215,7 @@ class Atom extends Model if openDevTools @openDevTools() - @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()') + @executeJavaScriptInDevTools('DevToolsAPI.showConsole()') @emit 'uncaught-error', arguments... if includeDeprecatedAPIs @emitter.emit 'did-throw-error', {message, url, line, column, originalError} @@ -586,12 +574,12 @@ class Atom extends Model # Call this method when establishing a real application window. startEditorWindow: -> - {resourcePath, safeMode} = @getLoadSettings() + {safeMode} = @getLoadSettings() CommandInstaller = require './command-installer' - CommandInstaller.installAtomCommand resourcePath, false, (error) -> + CommandInstaller.installAtomCommand false, (error) -> console.warn error.message if error? - CommandInstaller.installApmCommand resourcePath, false, (error) -> + CommandInstaller.installApmCommand false, (error) -> console.warn error.message if error? dimensions = @restoreWindowDimensions() @@ -791,11 +779,10 @@ class Atom extends Model dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} saveSync: -> - stateString = JSON.stringify(@state) - if statePath = @constructor.getStatePath(@project?.getPaths(), @mode) - fs.writeFileSync(statePath, stateString, 'utf8') + if storageKey = @constructor.getStateKey(@project?.getPaths(), @mode) + @constructor.getStorageFolder().store(storageKey, @state) else - @getCurrentWindow().loadSettings.windowState = stateString + @getCurrentWindow().loadSettings.windowState = JSON.stringify(@state) crashMainProcess: -> remote.process.crash() diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 2fee79f0b..dbe29caab 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -3,6 +3,7 @@ ApplicationMenu = require './application-menu' AtomProtocolHandler = require './atom-protocol-handler' AutoUpdateManager = require './auto-update-manager' BrowserWindow = require 'browser-window' +StorageFolder = require '../storage-folder' Menu = require 'menu' app = require 'app' fs = require 'fs-plus' @@ -43,7 +44,6 @@ class AtomApplication createAtomApplication() return - client = net.connect {path: options.socketPath}, -> client.write JSON.stringify(options), -> client.end() @@ -78,10 +78,13 @@ class AtomApplication @listenForArgumentsFromNewProcess() @setupJavaScriptArguments() @handleEvents() + @storageFolder = new StorageFolder(process.env.ATOM_HOME) - @openWithOptions(options) + if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test + @openWithOptions(options) + else + @loadState() or @openPath(options) - # Opens a new window based on the options provided. openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile}) -> if test @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) @@ -193,11 +196,14 @@ class AtomApplication @openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') @openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') - @openPathOnEvent('application:open-license', path.join(@resourcePath, 'LICENSE.md')) + @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) app.on 'window-all-closed', -> app.quit() if process.platform in ['win32', 'linux'] + app.on 'before-quit', => + @saveState() + app.on 'will-quit', => @killAllProcesses() @deleteSocketFile() @@ -328,7 +334,7 @@ class AtomApplication focusedWindow: -> _.find @windows, (atomWindow) -> atomWindow.isFocused() - # Public: Opens multiple paths, in existing windows if possible. + # Public: Opens a single path, in an existing window if possible. # # options - # :pathToOpen - The file path to open @@ -341,7 +347,7 @@ class AtomApplication openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) -> @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) - # Public: Opens a single path, in an existing window if possible. + # Public: Opens multiple paths, in existing windows if possible. # # options - # :pathsToOpen - The array of file paths to open @@ -411,6 +417,33 @@ class AtomApplication console.log("Killing process #{pid} failed: #{error.code ? error.message}") delete @pidsToOpenWindows[pid] + saveState: -> + states = [] + for window in @windows + if loadSettings = window.getLoadSettings() + unless loadSettings.isSpec + states.push(_.pick(loadSettings, + 'initialPaths' + 'devMode' + 'safeMode' + 'apiPreviewMode' + )) + @storageFolder.store('application.json', states) + + loadState: -> + if (states = @storageFolder.load('application.json'))?.length > 0 + for state in states + @openWithOptions({ + pathsToOpen: state.initialPaths + urlsToOpen: [] + devMode: state.devMode + safeMode: state.safeMode + apiPreviewMode: state.apiPreviewMode + }) + true + else + false + # Open an atom:// url. # # The host of the URL being opened is assumed to be the package name diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index fff98799c..6a6abb4b9 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -87,8 +87,9 @@ class AtomWindow hash: encodeURIComponent(JSON.stringify(loadSettings)) getLoadSettings: -> - hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1) - JSON.parse(decodeURIComponent(hash)) + if @browserWindow.webContents.loaded + hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1) + JSON.parse(decodeURIComponent(hash)) hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0 @@ -105,7 +106,7 @@ class AtomWindow true containsPath: (pathToCheck) -> - @getLoadSettings().initialPaths?.some (projectPath) -> + @getLoadSettings()?.initialPaths?.some (projectPath) -> if not projectPath false else if not pathToCheck diff --git a/src/command-installer.coffee b/src/command-installer.coffee index c0ed22482..afd5000c1 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -1,6 +1,4 @@ path = require 'path' -_ = require 'underscore-plus' -async = require 'async' fs = require 'fs-plus' runas = null # defer until used @@ -36,12 +34,11 @@ module.exports = message: "Failed to install shell commands" detailedMessage: error.message - resourcePath = atom.getLoadSettings().resourcePath - @installAtomCommand resourcePath, true, (error) => + @installAtomCommand true, (error) => if error? showErrorDialog(error) else - @installApmCommand resourcePath, true, (error) -> + @installApmCommand true, (error) -> if error? showErrorDialog(error) else @@ -49,12 +46,12 @@ module.exports = message: "Commands installed." detailedMessage: "The shell commands `atom` and `apm` are installed." - installAtomCommand: (resourcePath, askForPrivilege, callback) -> - commandPath = path.join(resourcePath, 'atom.sh') + installAtomCommand: (askForPrivilege, callback) -> + commandPath = path.join(process.resourcesPath, 'app', 'atom.sh') @createSymlink commandPath, askForPrivilege, callback - installApmCommand: (resourcePath, askForPrivilege, callback) -> - commandPath = path.join(resourcePath, 'apm', 'node_modules', '.bin', 'apm') + installApmCommand: (askForPrivilege, callback) -> + commandPath = path.join(process.resourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm') @createSymlink commandPath, askForPrivilege, callback createSymlink: (commandPath, askForPrivilege, callback) -> diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 386a65e68..8527188d3 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -111,7 +111,7 @@ class PackageManager commandName = 'apm' commandName += '.cmd' if process.platform is 'win32' - apmRoot = path.resolve(__dirname, '..', 'apm') + apmRoot = path.join(process.resourcesPath, 'app', 'apm') @apmPath = path.join(apmRoot, 'bin', commandName) unless fs.isFileSync(@apmPath) @apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName) diff --git a/src/package.coffee b/src/package.coffee index 74c4f0eb8..de90c6cb4 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -371,7 +371,12 @@ class Package reloadStylesheets: -> oldSheets = _.clone(@stylesheets) - @loadStylesheets() + + try + @loadStylesheets() + catch error + @handleError("Failed to reload the #{@name} package stylesheets", error) + @stylesheetDisposables?.dispose() @stylesheetDisposables = new CompositeDisposable @stylesheetsActivated = false diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee index 569c36534..f9dd15697 100644 --- a/src/pane-axis-element.coffee +++ b/src/pane-axis-element.coffee @@ -1,5 +1,6 @@ {CompositeDisposable} = require 'event-kit' {callAttachHooks} = require './space-pen-extensions' +PaneResizeHandleElement = require './pane-resize-handle-element' class PaneAxisElement extends HTMLElement createdCallback: -> @@ -12,6 +13,7 @@ class PaneAxisElement extends HTMLElement @subscriptions.add @model.onDidAddChild(@childAdded.bind(this)) @subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this)) @subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this)) + @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this)) @childAdded({child, index}) for child, index in @model.getChildren() @@ -22,13 +24,33 @@ class PaneAxisElement extends HTMLElement @classList.add('vertical', 'pane-column') this + isPaneResizeHandleElement: (element) -> + element?.nodeName.toLowerCase() is 'atom-pane-resize-handle' + childAdded: ({child, index}) -> view = atom.views.getView(child) - @insertBefore(view, @children[index]) + @insertBefore(view, @children[index * 2]) + + prevElement = view.previousSibling + # if previous element is not pane resize element, then insert new resize element + if prevElement? and not @isPaneResizeHandleElement(prevElement) + resizeHandle = document.createElement('atom-pane-resize-handle') + @insertBefore(resizeHandle, view) + + nextElement = view.nextSibling + # if next element isnot resize element, then insert new resize element + if nextElement? and not @isPaneResizeHandleElement(nextElement) + resizeHandle = document.createElement('atom-pane-resize-handle') + @insertBefore(resizeHandle, nextElement) + callAttachHooks(view) # for backward compatibility with SpacePen views childRemoved: ({child}) -> view = atom.views.getView(child) + siblingView = view.previousSibling + # make sure next sibling view is pane resize view + if siblingView? and @isPaneResizeHandleElement(siblingView) + siblingView.remove() view.remove() childReplaced: ({index, oldChild, newChild}) -> @@ -37,6 +59,8 @@ class PaneAxisElement extends HTMLElement @childAdded({child: newChild, index}) focusedElement?.focus() if document.activeElement is document.body + flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale + hasFocus: -> this is document.activeElement or @contains(document.activeElement) diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index ec77b17d2..1fba48d37 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -12,13 +12,14 @@ class PaneAxis extends Model container: null orientation: null - constructor: ({@container, @orientation, children}) -> + constructor: ({@container, @orientation, children, flexScale}={}) -> @emitter = new Emitter @subscriptionsByChild = new WeakMap @subscriptions = new CompositeDisposable @children = [] if children? @addChild(child) for child in children + @flexScale = flexScale ? 1 deserializeParams: (params) -> {container} = params @@ -28,6 +29,13 @@ class PaneAxis extends Model serializeParams: -> children: @children.map (child) -> child.serialize() orientation: @orientation + flexScale: @flexScale + + getFlexScale: -> @flexScale + + setFlexScale: (@flexScale) -> + @emitter.emit 'did-change-flex-scale', @flexScale + @flexScale getParent: -> @parent @@ -59,6 +67,13 @@ class PaneAxis extends Model onDidDestroy: (fn) -> @emitter.on 'did-destroy', fn + onDidChangeFlexScale: (fn) -> + @emitter.on 'did-change-flex-scale', fn + + observeFlexScale: (fn) -> + fn(@flexScale) + @onDidChangeFlexScale(fn) + addChild: (child, index=@children.length) -> child.setParent(this) child.setContainer(@container) @@ -68,6 +83,16 @@ class PaneAxis extends Model @children.splice(index, 0, child) @emitter.emit 'did-add-child', {child, index} + adjustFlexScale: -> + # get current total flex scale of children + total = 0 + total += child.getFlexScale() for child in @children + + needTotal = @children.length + # set every child's flex scale by the ratio + for child in @children + child.setFlexScale(needTotal * child.getFlexScale() / total) + removeChild: (child, replacing=false) -> index = @children.indexOf(child) throw new Error("Removing non-existent child") if index is -1 @@ -75,6 +100,7 @@ class PaneAxis extends Model @unsubscribeFromChild(child) @children.splice(index, 1) + @adjustFlexScale() @emitter.emit 'did-remove-child', {child, index} @reparentLastChild() if not replacing and @children.length < 2 @@ -98,7 +124,9 @@ class PaneAxis extends Model @addChild(newChild, index + 1) reparentLastChild: -> - @parent.replaceChild(this, @children[0]) + lastChild = @children[0] + lastChild.setFlexScale(@flexScale) + @parent.replaceChild(this, lastChild) @destroy() subscribeToChild: (child) -> diff --git a/src/pane-element.coffee b/src/pane-element.coffee index f5bfc18bd..15990bcc2 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -1,3 +1,4 @@ +path = require 'path' {CompositeDisposable} = require 'event-kit' Grim = require 'grim' {$, callAttachHooks, callRemoveHooks} = require './space-pen-extensions' @@ -38,8 +39,21 @@ class PaneElement extends HTMLElement handleBlur = (event) => @model.blur() unless @contains(event.relatedTarget) + handleDragOver = (event) -> + event.preventDefault() + event.stopPropagation() + + handleDrop = (event) => + event.preventDefault() + event.stopPropagation() + @getModel().activate() + pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path + atom.open({pathsToOpen}) if pathsToOpen.length > 0 + @addEventListener 'focus', handleFocus, true @addEventListener 'blur', handleBlur, true + @addEventListener 'dragover', handleDragOver + @addEventListener 'drop', handleDrop createSpacePenShim: -> PaneView ?= require './pane-view' @@ -51,6 +65,8 @@ class PaneElement extends HTMLElement @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this)) @subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this)) @subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this)) + @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this)) + @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs this @@ -66,11 +82,18 @@ class PaneElement extends HTMLElement @classList.remove('active') activeItemChanged: (item) -> + delete @dataset.activeItemName + delete @dataset.activeItemPath + return unless item? hasFocus = @hasFocus() itemView = atom.views.getView(item) + if itemPath = item.getPath?() + @dataset.activeItemName = path.basename(itemPath) + @dataset.activeItemPath = itemPath + unless @itemViews.contains(itemView) @itemViews.appendChild(itemView) callAttachHooks(itemView) @@ -104,6 +127,9 @@ class PaneElement extends HTMLElement paneDestroyed: -> @subscriptions.dispose() + flexScaleChanged: (flexScale) -> + @style.flexGrow = flexScale + getActiveView: -> atom.views.getView(@model.getActiveItem()) hasFocus: -> diff --git a/src/pane-resize-handle-element.coffee b/src/pane-resize-handle-element.coffee new file mode 100644 index 000000000..47548bc60 --- /dev/null +++ b/src/pane-resize-handle-element.coffee @@ -0,0 +1,64 @@ +class PaneResizeHandleElement extends HTMLElement + createdCallback: -> + @resizePane = @resizePane.bind(this) + @resizeStopped = @resizeStopped.bind(this) + @subscribeToDOMEvents() + + subscribeToDOMEvents: -> + @addEventListener 'dblclick', @resizeToFitContent.bind(this) + @addEventListener 'mousedown', @resizeStarted.bind(this) + + attachedCallback: -> + @isHorizontal = @parentElement.classList.contains("horizontal") + @classList.add if @isHorizontal then 'horizontal' else 'vertical' + + resizeToFitContent: -> + # clear flex-grow css style of both pane + @previousSibling.model.setFlexScale(1) + @nextSibling.model.setFlexScale(1) + + resizeStarted: (e) -> + e.stopPropagation() + document.addEventListener 'mousemove', @resizePane + document.addEventListener 'mouseup', @resizeStopped + + resizeStopped: -> + document.removeEventListener 'mousemove', @resizePane + document.removeEventListener 'mouseup', @resizeStopped + + calcRatio: (ratio1, ratio2, total) -> + allRatio = ratio1 + ratio2 + [total * ratio1 / allRatio, total * ratio2 / allRatio] + + setFlexGrow: (prevSize, nextSize) -> + @prevModel = @previousSibling.model + @nextModel = @nextSibling.model + totalScale = @prevModel.getFlexScale() + @nextModel.getFlexScale() + flexGrows = @calcRatio(prevSize, nextSize, totalScale) + @prevModel.setFlexScale flexGrows[0] + @nextModel.setFlexScale flexGrows[1] + + fixInRange: (val, minValue, maxValue) -> + Math.min(Math.max(val, minValue), maxValue) + + resizePane: ({clientX, clientY, which}) -> + return @resizeStopped() unless which is 1 + + if @isHorizontal + totalWidth = @previousSibling.clientWidth + @nextSibling.clientWidth + #get the left and right width after move the resize view + leftWidth = clientX - @previousSibling.getBoundingClientRect().left + leftWidth = @fixInRange(leftWidth, 0, totalWidth) + rightWidth = totalWidth - leftWidth + # set the flex grow by the ratio of left width and right width + # to change pane width + @setFlexGrow(leftWidth, rightWidth) + else + totalHeight = @previousSibling.clientHeight + @nextSibling.clientHeight + topHeight = clientY - @previousSibling.getBoundingClientRect().top + topHeight = @fixInRange(topHeight, 0, totalHeight) + bottomHeight = totalHeight - topHeight + @setFlexGrow(topHeight, bottomHeight) + +module.exports = PaneResizeHandleElement = +document.registerElement 'atom-pane-resize-handle', prototype: PaneResizeHandleElement.prototype diff --git a/src/pane.coffee b/src/pane.coffee index 0d8c68066..38c8d9201 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -28,6 +28,7 @@ class Pane extends Model @addItems(compact(params?.items ? [])) @setActiveItem(@items[0]) unless @getActiveItem()? + @setFlexScale(params?.flexScale ? 1) # Called by the Serializable mixin during serialization. serializeParams: -> @@ -40,6 +41,7 @@ class Pane extends Model items: compact(@items.map((item) -> item.serialize?())) activeItemURI: activeItemURI focused: @focused + flexScale: @flexScale # Called by the Serializable mixin during deserialization. deserializeParams: (params) -> @@ -66,10 +68,36 @@ class Pane extends Model @container = container container.didAddPane({pane: this}) + setFlexScale: (@flexScale) -> + @emitter.emit 'did-change-flex-scale', @flexScale + @flexScale + + getFlexScale: -> @flexScale ### Section: Event Subscription ### + # Public: Invoke the given callback when the pane resize + # + # the callback will be invoked when pane's flexScale property changes + # + # * `callback` {Function} to be called when the pane is resized + # + # Returns a {Disposable} on which '.dispose()' can be called to unsubscribe. + onDidChangeFlexScale: (callback) -> + @emitter.on 'did-change-flex-scale', callback + + # Public: Invoke the given callback with all current and future items. + # + # * `callback` {Function} to be called with current and future items. + # * `item` An item that is present in {::getItems} at the time of + # subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeFlexScale: (callback) -> + callback(@flexScale) + @onDidChangeFlexScale(callback) + # Public: Invoke the given callback when the pane is activated. # # The given callback will be invoked whenever {::activate} is called on the @@ -591,7 +619,8 @@ class Pane extends Model params.items.push(@copyActiveItem()) if @parent.orientation isnt orientation - @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) + @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale})) + @setFlexScale(1) newPane = new @constructor(params) switch side diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee new file mode 100644 index 000000000..bf969dee2 --- /dev/null +++ b/src/storage-folder.coffee @@ -0,0 +1,27 @@ +path = require "path" +fs = require "fs-plus" + +module.exports = +class StorageFolder + constructor: (containingPath) -> + @path = path.join(containingPath, "storage") + + store: (name, object) -> + fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8') + + load: (name) -> + statePath = @pathForKey(name) + try + stateString = fs.readFileSync(statePath, 'utf8') + catch error + unless error.code is 'ENOENT' + console.warn "Error reading state file: #{statePath}", error.stack, error + return undefined + + try + JSON.parse(stateString) + catch error + console.warn "Error parsing state file: #{statePath}", error.stack, error + + pathForKey: (name) -> path.join(@getPath(), name) + getPath: -> @path diff --git a/src/task.coffee b/src/task.coffee index 9572494b8..d752ea11d 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -83,7 +83,7 @@ class Task taskPath = taskPath.replace(/\\/g, "\\\\") env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent}) - @childProcess = fork '--eval', [bootstrap], {env, cwd: __dirname} + @childProcess = fork '--eval', [bootstrap], {env, silent: true} @on "task:log", -> console.log(arguments...) @on "task:warn", -> console.warn(arguments...) @@ -100,6 +100,11 @@ class Task @childProcess.removeAllListeners() @childProcess.on 'message', ({event, args}) => @emit(event, args...) if @childProcess? + # Catch the errors that happened before task-bootstrap. + @childProcess.stdout.on 'data', (data) -> + console.log data.toString() + @childProcess.stderr.on 'data', (data) -> + console.error data.toString() # Public: Starts the task. # diff --git a/src/theme-package.coffee b/src/theme-package.coffee index e1b87e783..f14750d2f 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -14,11 +14,7 @@ class ThemePackage extends Package atom.config.removeAtKeyPath('core.themes', @name) load: -> - @measure 'loadTime', => - try - @metadata ?= Package.loadMetadata(@path) - catch error - console.warn "Failed to load theme named '#{@name}'", error.stack ? error + @loadTime = 0 this activate: -> @@ -26,7 +22,10 @@ class ThemePackage extends Package @activationDeferred = Q.defer() @measure 'activateTime', => - @loadStylesheets() - @activateNow() + try + @loadStylesheets() + @activateNow() + catch error + @handleError("Failed to activate the #{@name} theme", error) @activationDeferred.promise diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 96518c446..7d67e87ab 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -1,6 +1,5 @@ path = require 'path' {$} = require './space-pen-extensions' -_ = require 'underscore-plus' {Disposable} = require 'event-kit' ipc = require 'ipc' shell = require 'shell' @@ -136,12 +135,11 @@ class WindowEventHandler onDrop: (event) -> event.preventDefault() event.stopPropagation() - pathsToOpen = _.pluck(event.dataTransfer.files, 'path') - atom.open({pathsToOpen}) if pathsToOpen.length > 0 onDragOver: (event) -> event.preventDefault() event.stopPropagation() + event.dataTransfer.dropEffect = 'none' openLink: ({target, currentTarget}) -> location = target?.getAttribute('href') or currentTarget?.getAttribute('href') diff --git a/static/panes.less b/static/panes.less index bf275e6da..163721fcc 100644 --- a/static/panes.less +++ b/static/panes.less @@ -4,19 +4,42 @@ // settings-view, the archive-view, the image-view. Etc. Basically a non- // editor resource with a tab. atom-pane-container { + position: relative; display: -webkit-flex; -webkit-flex: 1; - atom-pane-axis.vertical { + atom-pane-axis { display: -webkit-flex; -webkit-flex: 1; + + & > atom-pane-resize-handle { + position: absolute; + z-index: 3; + } + } + + atom-pane-axis.vertical { -webkit-flex-direction: column; + + & > atom-pane-resize-handle { + width: 100%; + height: 8px; + margin-top: -4px; + cursor: ns-resize; + border-bottom: none; + } } atom-pane-axis.horizontal { - display: -webkit-flex; - -webkit-flex: 1; -webkit-flex-direction: row; + + & > atom-pane-resize-handle { + width: 8px; + height: 100%; + margin-left: -4px; + cursor: ew-resize; + border-right: none; + } } atom-pane {