From a874c98fbdb35bf6be80f46b548e5c18b5cc6f5b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 25 Sep 2018 12:15:42 -0700 Subject: [PATCH 1/3] :arrow_right: Migrate core package 'link' into ./packages --- package-lock.json | 3 +- package.json | 4 +- packages/README.md | 2 +- packages/link/.gitignore | 2 + packages/link/.npmignore | 1 + packages/link/LICENSE.md | 20 ++++ packages/link/README.md | 11 ++ packages/link/keymaps/links.cson | 2 + packages/link/lib/link.js | 75 +++++++++++++ packages/link/menus/link.cson | 4 + packages/link/package.json | 33 ++++++ packages/link/spec/async-spec-helpers.js | 103 ++++++++++++++++++ packages/link/spec/link-spec.js | 131 +++++++++++++++++++++++ 13 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 packages/link/.gitignore create mode 100644 packages/link/.npmignore create mode 100644 packages/link/LICENSE.md create mode 100644 packages/link/README.md create mode 100644 packages/link/keymaps/links.cson create mode 100644 packages/link/lib/link.js create mode 100644 packages/link/menus/link.cson create mode 100644 packages/link/package.json create mode 100644 packages/link/spec/async-spec-helpers.js create mode 100644 packages/link/spec/link-spec.js diff --git a/package-lock.json b/package-lock.json index e212703a3..361eb494c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3477,8 +3477,7 @@ } }, "link": { - "version": "https://www.atom.io/api/packages/link/versions/0.31.6/tarball", - "integrity": "sha512-+LqJ1Iv9bPTeovPSO7uOgEID3MgKVntaB7DjVWD1JLOCbaLPdqHHOmCRHifhsAE8cVm3HxdxLo7N1vmBXF+TUg==", + "version": "file:packages/link", "requires": { "underscore-plus": "1.x" } diff --git a/package.json b/package.json index a6e89b59d..4ec3c9cef 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "less-cache": "1.1.0", "line-ending-selector": "https://www.atom.io/api/packages/line-ending-selector/versions/0.7.7/tarball", "line-top-index": "0.3.1", - "link": "https://www.atom.io/api/packages/link/versions/0.31.6/tarball", + "link": "file:packages/link", "markdown-preview": "https://www.atom.io/api/packages/markdown-preview/versions/0.159.25/tarball", "marked": "^0.3.12", "metrics": "https://www.atom.io/api/packages/metrics/versions/1.6.2/tarball", @@ -210,7 +210,7 @@ "incompatible-packages": "file:./packages/incompatible-packages", "keybinding-resolver": "0.38.4", "line-ending-selector": "0.7.7", - "link": "0.31.6", + "link": "file:./packages/link", "markdown-preview": "0.159.25", "metrics": "1.6.2", "notifications": "0.70.5", diff --git a/packages/README.md b/packages/README.md index b88d01d34..208a48de1 100644 --- a/packages/README.md +++ b/packages/README.md @@ -74,7 +74,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **language-xml** | [`atom/language-xml`][language-xml] | | | **language-yaml** | [`atom/language-yaml`][language-yaml] | | | **line-ending-selector** | [`atom/line-ending-selector`][line-ending-selector] | [#17847](https://github.com/atom/atom/issues/17847) | -| **link** | [`atom/link`][link] | [#17848](https://github.com/atom/atom/issues/17848) | +| **link** | [`./packages/link`][./link] | [#17848](https://github.com/atom/atom/issues/17848) | | **markdown-preview** | [`atom/markdown-preview`][markdown-preview] | | | **metrics** | [`atom/metrics`][metrics] | | | **notifications** | [`atom/notifications`][notifications] | | diff --git a/packages/link/.gitignore b/packages/link/.gitignore new file mode 100644 index 000000000..93f136199 --- /dev/null +++ b/packages/link/.gitignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/packages/link/.npmignore b/packages/link/.npmignore new file mode 100644 index 000000000..c8f50f7cd --- /dev/null +++ b/packages/link/.npmignore @@ -0,0 +1 @@ +npm-debug.log diff --git a/packages/link/LICENSE.md b/packages/link/LICENSE.md new file mode 100644 index 000000000..4d231b456 --- /dev/null +++ b/packages/link/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/link/README.md b/packages/link/README.md new file mode 100644 index 000000000..b7ae6fae2 --- /dev/null +++ b/packages/link/README.md @@ -0,0 +1,11 @@ +# Link package + +Opens http(s) links under the cursor. + +### Commands and Keybindings + +|Command|Selector|Description|Keybinding (Linux)|Keybinding (macOS)|Keybinding (Windows)| +|-------|--------|-----------|------------------|------------------|--------------------| +|`link:open`|`atom-text-editor`|Opens the http(s) link under the cursor||ctrl-shift-o|| + +Custom keybindings can be added by referencing the above commands. To learn more, visit the [Using Atom: Basic Customization](http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings) or [Behind Atom: Keymaps In-Depth](http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth) sections of the Atom Flight Manual. diff --git a/packages/link/keymaps/links.cson b/packages/link/keymaps/links.cson new file mode 100644 index 000000000..7919d4c6a --- /dev/null +++ b/packages/link/keymaps/links.cson @@ -0,0 +1,2 @@ +'.platform-darwin atom-text-editor': + 'ctrl-shift-o': 'link:open' diff --git a/packages/link/lib/link.js b/packages/link/lib/link.js new file mode 100644 index 000000000..4b2443cfb --- /dev/null +++ b/packages/link/lib/link.js @@ -0,0 +1,75 @@ +const url = require('url') +const {shell} = require('electron') +const _ = require('underscore-plus') + +const LINK_SCOPE_REGEX = /markup\.underline\.link/ + +module.exports = { + activate () { + this.commandDisposable = atom.commands.add('atom-text-editor', 'link:open', () => this.openLink()) + }, + + deactivate () { + this.commandDisposable.dispose() + }, + + openLink () { + const editor = atom.workspace.getActiveTextEditor() + if (editor == null) return + + let link = this.linkUnderCursor(editor) + if (link == null) return + + if (editor.getGrammar().scopeName === 'source.gfm') { + link = this.linkForName(editor, link) + } + + const {protocol} = url.parse(link) + if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') shell.openExternal(link) + }, + + // Get the link under the cursor in the editor + // + // Returns a {String} link or undefined if no link found. + linkUnderCursor (editor) { + const cursorPosition = editor.getCursorBufferPosition() + const link = this.linkAtPosition(editor, cursorPosition) + if (link != null) return link + + // Look for a link to the left of the cursor + if (cursorPosition.column > 0) { + return this.linkAtPosition(editor, cursorPosition.translate([0, -1])) + } + }, + + // Get the link at the buffer position in the editor. + // + // Returns a {String} link or undefined if no link found. + linkAtPosition (editor, bufferPosition) { + const token = editor.tokenForBufferPosition(bufferPosition) + if (token && token.value && token.scopes.some(scope => LINK_SCOPE_REGEX.test(scope))) { + return token.value + } + }, + + // Get the link for the given name. + // + // This is for Markdown links of the style: + // + // ``` + // [label][name] + // + // [name]: https://github.com + // ``` + // + // Returns a {String} link + linkForName (editor, linkName) { + let link = linkName + const regex = new RegExp(`^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`, 'g') + editor.backwardsScanInBufferRange(regex, [[0, 0], [Infinity, Infinity]], ({match, stop}) => { + link = match[1] + stop() + }) + return link + } +} diff --git a/packages/link/menus/link.cson b/packages/link/menus/link.cson new file mode 100644 index 000000000..aa0a4b25d --- /dev/null +++ b/packages/link/menus/link.cson @@ -0,0 +1,4 @@ +'context-menu': + 'atom-text-editor .syntax--markup.syntax--underline.syntax--link': [ + {label: 'Open link', command: 'link:open'} + ] diff --git a/packages/link/package.json b/packages/link/package.json new file mode 100644 index 000000000..ac1a3b04f --- /dev/null +++ b/packages/link/package.json @@ -0,0 +1,33 @@ +{ + "name": "link", + "version": "0.31.6", + "main": "./lib/link", + "description": "Opens http(s) links under the cursor", + "license": "MIT", + "repository": "https://github.com/atom/atom", + "engines": { + "atom": "*" + }, + "activationCommands": { + "atom-workspace": [ + "link:open" + ] + }, + "dependencies": { + "underscore-plus": "1.x" + }, + "devDependencies": { + "standard": "^10.0.3" + }, + "standard": { + "env": { + "atomtest": true, + "browser": true, + "jasmine": true, + "node": true + }, + "globals": [ + "atom" + ] + } +} diff --git a/packages/link/spec/async-spec-helpers.js b/packages/link/spec/async-spec-helpers.js new file mode 100644 index 000000000..73002c049 --- /dev/null +++ b/packages/link/spec/async-spec-helpers.js @@ -0,0 +1,103 @@ +/** @babel */ + +export function beforeEach (fn) { + global.beforeEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +export function afterEach (fn) { + global.afterEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { + module.exports[name] = function (description, fn) { + if (fn === undefined) { + global[name](description) + return + } + + global[name](description, function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) + } +}) + +export async function conditionPromise (condition, description = 'anonymous condition') { + const startTime = Date.now() + + while (true) { + await timeoutPromise(100) + + if (await condition()) { + return + } + + if (Date.now() - startTime > 5000) { + throw new Error('Timed out waiting on ' + description) + } + } +} + +export function timeoutPromise (timeout) { + return new Promise(function (resolve) { + global.setTimeout(resolve, timeout) + }) +} + +function waitsForPromise (fn) { + const promise = fn() + global.waitsFor('spec promise to resolve', function (done) { + promise.then(done, function (error) { + jasmine.getEnv().currentSpec.fail(error) + done() + }) + }) +} + +export function emitterEventPromise (emitter, event, timeout = 15000) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + reject(new Error(`Timed out waiting for '${event}' event`)) + }, timeout) + emitter.once(event, () => { + clearTimeout(timeoutHandle) + resolve() + }) + }) +} + +export function promisify (original) { + return function (...args) { + return new Promise((resolve, reject) => { + args.push((err, ...results) => { + if (err) { + reject(err) + } else { + resolve(...results) + } + }) + + return original(...args) + }) + } +} + +export function promisifySome (obj, fnNames) { + const result = {} + for (const fnName of fnNames) { + result[fnName] = promisify(obj[fnName]) + } + return result +} diff --git a/packages/link/spec/link-spec.js b/packages/link/spec/link-spec.js new file mode 100644 index 000000000..b5024f5d8 --- /dev/null +++ b/packages/link/spec/link-spec.js @@ -0,0 +1,131 @@ +const {shell} = require('electron') + +const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars + +describe('link package', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-gfm') + await atom.packages.activatePackage('language-hyperlink') + + const activationPromise = atom.packages.activatePackage('link') + atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open') + await activationPromise + }) + + describe('when the cursor is on a link', () => { + it("opens the link using the 'open' command", async () => { + await atom.workspace.open('sample.md') + + const editor = atom.workspace.getActiveTextEditor() + editor.setText('// "http://github.com"') + + spyOn(shell, 'openExternal') + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + expect(shell.openExternal).not.toHaveBeenCalled() + + editor.setCursorBufferPosition([0, 4]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') + + shell.openExternal.reset() + editor.setCursorBufferPosition([0, 8]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') + + shell.openExternal.reset() + editor.setCursorBufferPosition([0, 21]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') + }) + + // only works in Atom >= 1.32.0 + // https://github.com/atom/link/pull/33#issuecomment-419643655 + const atomVersion = atom.getVersion().split('.') + if (+atomVersion[0] > 1 || +atomVersion[1] >= 32) { + it("opens an 'atom:' link", async () => { + await atom.workspace.open('sample.md') + + const editor = atom.workspace.getActiveTextEditor() + editor.setText('// "atom://core/open/file?filename=sample.js&line=1&column=2"') + + spyOn(shell, 'openExternal') + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + expect(shell.openExternal).not.toHaveBeenCalled() + + editor.setCursorBufferPosition([0, 4]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') + + shell.openExternal.reset() + editor.setCursorBufferPosition([0, 8]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') + + shell.openExternal.reset() + editor.setCursorBufferPosition([0, 60]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') + }) + } + + describe('when the cursor is on a [name][url-name] style markdown link', () => + it('opens the named url', async () => { + await atom.workspace.open('README.md') + + const editor = atom.workspace.getActiveTextEditor() + editor.setText(`\ +you should [click][here] +you should not [click][her] + +[here]: http://github.com\ +` + ) + + spyOn(shell, 'openExternal') + editor.setCursorBufferPosition([0, 0]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + expect(shell.openExternal).not.toHaveBeenCalled() + + editor.setCursorBufferPosition([0, 20]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') + + shell.openExternal.reset() + editor.setCursorBufferPosition([1, 24]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).not.toHaveBeenCalled() + }) + ) + + it('does not open non http/https/atom links', async () => { + await atom.workspace.open('sample.md') + + const editor = atom.workspace.getActiveTextEditor() + editor.setText('// ftp://github.com\n') + + spyOn(shell, 'openExternal') + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + expect(shell.openExternal).not.toHaveBeenCalled() + + editor.setCursorBufferPosition([0, 5]) + atom.commands.dispatch(atom.views.getView(editor), 'link:open') + + expect(shell.openExternal).not.toHaveBeenCalled() + }) + }) +}) From bf053072a70e364016d2dfbfc33c2354346d55a4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 26 Sep 2018 10:59:21 -0700 Subject: [PATCH 2/3] :arrow_up: language-hyperlink --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 361eb494c..64434d01e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3181,8 +3181,8 @@ } }, "language-hyperlink": { - "version": "https://www.atom.io/api/packages/language-hyperlink/versions/0.16.3/tarball", - "integrity": "sha512-IDkh820N85GVgcP0EiU2QceAcmRHyYQCzJkaG7eSwWmOxvf5e+bO9g2U28sED14hQjH+No4MRfU5+grEmAnvuw==" + "version": "https://www.atom.io/api/packages/language-hyperlink/versions/0.17.0/tarball", + "integrity": "sha512-V7IEqrIvn75LX/iQ/MPA75nKdfQ3kfJ5zWvUoCAxfvE9tJ6X25eSvqqgp10zxI0yjB9AQ/JDywYsKd5fEmQ8NQ==" }, "language-java": { "version": "https://www.atom.io/api/packages/language-java/versions/0.30.0/tarball", diff --git a/package.json b/package.json index 4ec3c9cef..204b44350 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "language-git": "https://www.atom.io/api/packages/language-git/versions/0.19.1/tarball", "language-go": "https://www.atom.io/api/packages/language-go/versions/0.46.3/tarball", "language-html": "https://www.atom.io/api/packages/language-html/versions/0.51.5/tarball", - "language-hyperlink": "https://www.atom.io/api/packages/language-hyperlink/versions/0.16.3/tarball", + "language-hyperlink": "https://www.atom.io/api/packages/language-hyperlink/versions/0.17.0/tarball", "language-java": "https://www.atom.io/api/packages/language-java/versions/0.30.0/tarball", "language-javascript": "https://www.atom.io/api/packages/language-javascript/versions/0.129.9/tarball", "language-json": "https://www.atom.io/api/packages/language-json/versions/0.19.2/tarball", @@ -238,7 +238,7 @@ "language-git": "0.19.1", "language-go": "0.46.3", "language-html": "0.51.5", - "language-hyperlink": "0.16.3", + "language-hyperlink": "0.17.0", "language-java": "0.30.0", "language-javascript": "0.129.9", "language-json": "0.19.2", From 65eb83510dca57a157598632a9f6ec4a35a28fe1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 26 Sep 2018 10:59:50 -0700 Subject: [PATCH 3/3] Update version guard in some link specs to 1.33.0 --- packages/link/spec/link-spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/link/spec/link-spec.js b/packages/link/spec/link-spec.js index b5024f5d8..976261cd4 100644 --- a/packages/link/spec/link-spec.js +++ b/packages/link/spec/link-spec.js @@ -44,10 +44,11 @@ describe('link package', () => { expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') }) - // only works in Atom >= 1.32.0 + // only works in Atom >= 1.33.0 // https://github.com/atom/link/pull/33#issuecomment-419643655 const atomVersion = atom.getVersion().split('.') - if (+atomVersion[0] > 1 || +atomVersion[1] >= 32) { + console.error("atomVersion", atomVersion) + if (+atomVersion[0] > 1 || +atomVersion[1] >= 33) { it("opens an 'atom:' link", async () => { await atom.workspace.open('sample.md')