├── .github └── no-response.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── appveyor.yml ├── keymaps └── links.cson ├── lib └── link.js ├── menus └── link.cson ├── package.json └── spec ├── async-spec-helpers.js └── link-spec.js /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 180 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | - ATOM_CHANNEL=beta 12 | 13 | ### Generic setup follows ### 14 | script: 15 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 16 | - chmod u+x build-package.sh 17 | - ./build-package.sh 18 | 19 | notifications: 20 | email: 21 | on_success: never 22 | on_failure: change 23 | 24 | branches: 25 | only: 26 | - master 27 | 28 | git: 29 | depth: 10 30 | 31 | sudo: false 32 | 33 | dist: trusty 34 | 35 | addons: 36 | apt: 37 | packages: 38 | - build-essential 39 | - fakeroot 40 | - git 41 | - libsecret-1-dev 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This package is now a part of the [core Atom repository](https://github.com/atom/atom/tree/master/packages/link), please direct all issues and pull requests there in the future! 2 | 3 | --- 4 | 5 | # Link package 6 | [![macOS Build Status](https://travis-ci.org/atom/link.svg?branch=master)](https://travis-ci.org/atom/link) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1d3cb8ktd48k9vnl/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/link/branch/master) [![Dependency Status](https://david-dm.org/atom/link.svg)](https://david-dm.org/atom/link) 7 | 8 | Opens http(s) links under the cursor. 9 | 10 | ### Commands and Keybindings 11 | 12 | |Command|Selector|Description|Keybinding (Linux)|Keybinding (macOS)|Keybinding (Windows)| 13 | |-------|--------|-----------|------------------|------------------|--------------------| 14 | |`link:open`|`atom-text-editor`|Opens the http(s) link under the cursor||ctrl-shift-o|| 15 | 16 | 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. 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | - x64 3 | - x86 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | clone_depth: 10 10 | 11 | skip_tags: true 12 | 13 | environment: 14 | APM_TEST_PACKAGES: 15 | 16 | matrix: 17 | - ATOM_CHANNEL: stable 18 | - ATOM_CHANNEL: beta 19 | 20 | install: 21 | - ps: Install-Product node 6 22 | 23 | build_script: 24 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 25 | 26 | test: off 27 | deploy: off 28 | -------------------------------------------------------------------------------- /keymaps/links.cson: -------------------------------------------------------------------------------- 1 | '.platform-darwin atom-text-editor': 2 | 'ctrl-shift-o': 'link:open' 3 | -------------------------------------------------------------------------------- /lib/link.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const {shell} = require('electron') 3 | const _ = require('underscore-plus') 4 | 5 | const LINK_SCOPE_REGEX = /markup\.underline\.link/ 6 | 7 | module.exports = { 8 | activate () { 9 | this.commandDisposable = atom.commands.add('atom-text-editor', 'link:open', () => this.openLink()) 10 | }, 11 | 12 | deactivate () { 13 | this.commandDisposable.dispose() 14 | }, 15 | 16 | openLink () { 17 | const editor = atom.workspace.getActiveTextEditor() 18 | if (editor == null) return 19 | 20 | let link = this.linkUnderCursor(editor) 21 | if (link == null) return 22 | 23 | if (editor.getGrammar().scopeName === 'source.gfm') { 24 | link = this.linkForName(editor, link) 25 | } 26 | 27 | const {protocol} = url.parse(link) 28 | if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') shell.openExternal(link) 29 | }, 30 | 31 | // Get the link under the cursor in the editor 32 | // 33 | // Returns a {String} link or undefined if no link found. 34 | linkUnderCursor (editor) { 35 | const cursorPosition = editor.getCursorBufferPosition() 36 | const link = this.linkAtPosition(editor, cursorPosition) 37 | if (link != null) return link 38 | 39 | // Look for a link to the left of the cursor 40 | if (cursorPosition.column > 0) { 41 | return this.linkAtPosition(editor, cursorPosition.translate([0, -1])) 42 | } 43 | }, 44 | 45 | // Get the link at the buffer position in the editor. 46 | // 47 | // Returns a {String} link or undefined if no link found. 48 | linkAtPosition (editor, bufferPosition) { 49 | const token = editor.tokenForBufferPosition(bufferPosition) 50 | if (token && token.value && token.scopes.some(scope => LINK_SCOPE_REGEX.test(scope))) { 51 | return token.value 52 | } 53 | }, 54 | 55 | // Get the link for the given name. 56 | // 57 | // This is for Markdown links of the style: 58 | // 59 | // ``` 60 | // [label][name] 61 | // 62 | // [name]: https://github.com 63 | // ``` 64 | // 65 | // Returns a {String} link 66 | linkForName (editor, linkName) { 67 | let link = linkName 68 | const regex = new RegExp(`^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`, 'g') 69 | editor.backwardsScanInBufferRange(regex, [[0, 0], [Infinity, Infinity]], ({match, stop}) => { 70 | link = match[1] 71 | stop() 72 | }) 73 | return link 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /menus/link.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | 'atom-text-editor .syntax--markup.syntax--underline.syntax--link': [ 3 | {label: 'Open link', command: 'link:open'} 4 | ] 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "link", 3 | "version": "0.31.6", 4 | "main": "./lib/link", 5 | "description": "Opens http(s) links under the cursor", 6 | "license": "MIT", 7 | "repository": "https://github.com/atom/link", 8 | "engines": { 9 | "atom": "*" 10 | }, 11 | "activationCommands": { 12 | "atom-workspace": [ 13 | "link:open" 14 | ] 15 | }, 16 | "dependencies": { 17 | "underscore-plus": "1.x" 18 | }, 19 | "devDependencies": { 20 | "standard": "^10.0.3" 21 | }, 22 | "standard": { 23 | "env": { 24 | "atomtest": true, 25 | "browser": true, 26 | "jasmine": true, 27 | "node": true 28 | }, 29 | "globals": [ 30 | "atom" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/async-spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | export function beforeEach (fn) { 4 | global.beforeEach(function () { 5 | const result = fn() 6 | if (result instanceof Promise) { 7 | waitsForPromise(() => result) 8 | } 9 | }) 10 | } 11 | 12 | export function afterEach (fn) { 13 | global.afterEach(function () { 14 | const result = fn() 15 | if (result instanceof Promise) { 16 | waitsForPromise(() => result) 17 | } 18 | }) 19 | } 20 | 21 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { 22 | module.exports[name] = function (description, fn) { 23 | if (fn === undefined) { 24 | global[name](description) 25 | return 26 | } 27 | 28 | global[name](description, function () { 29 | const result = fn() 30 | if (result instanceof Promise) { 31 | waitsForPromise(() => result) 32 | } 33 | }) 34 | } 35 | }) 36 | 37 | export async function conditionPromise (condition, description = 'anonymous condition') { 38 | const startTime = Date.now() 39 | 40 | while (true) { 41 | await timeoutPromise(100) 42 | 43 | if (await condition()) { 44 | return 45 | } 46 | 47 | if (Date.now() - startTime > 5000) { 48 | throw new Error('Timed out waiting on ' + description) 49 | } 50 | } 51 | } 52 | 53 | export function timeoutPromise (timeout) { 54 | return new Promise(function (resolve) { 55 | global.setTimeout(resolve, timeout) 56 | }) 57 | } 58 | 59 | function waitsForPromise (fn) { 60 | const promise = fn() 61 | global.waitsFor('spec promise to resolve', function (done) { 62 | promise.then(done, function (error) { 63 | jasmine.getEnv().currentSpec.fail(error) 64 | done() 65 | }) 66 | }) 67 | } 68 | 69 | export function emitterEventPromise (emitter, event, timeout = 15000) { 70 | return new Promise((resolve, reject) => { 71 | const timeoutHandle = setTimeout(() => { 72 | reject(new Error(`Timed out waiting for '${event}' event`)) 73 | }, timeout) 74 | emitter.once(event, () => { 75 | clearTimeout(timeoutHandle) 76 | resolve() 77 | }) 78 | }) 79 | } 80 | 81 | export function promisify (original) { 82 | return function (...args) { 83 | return new Promise((resolve, reject) => { 84 | args.push((err, ...results) => { 85 | if (err) { 86 | reject(err) 87 | } else { 88 | resolve(...results) 89 | } 90 | }) 91 | 92 | return original(...args) 93 | }) 94 | } 95 | } 96 | 97 | export function promisifySome (obj, fnNames) { 98 | const result = {} 99 | for (const fnName of fnNames) { 100 | result[fnName] = promisify(obj[fnName]) 101 | } 102 | return result 103 | } 104 | -------------------------------------------------------------------------------- /spec/link-spec.js: -------------------------------------------------------------------------------- 1 | const {shell} = require('electron') 2 | 3 | const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars 4 | 5 | describe('link package', () => { 6 | beforeEach(async () => { 7 | await atom.packages.activatePackage('language-gfm') 8 | await atom.packages.activatePackage('language-hyperlink') 9 | 10 | const activationPromise = atom.packages.activatePackage('link') 11 | atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open') 12 | await activationPromise 13 | }) 14 | 15 | describe('when the cursor is on a link', () => { 16 | it("opens the link using the 'open' command", async () => { 17 | await atom.workspace.open('sample.md') 18 | 19 | const editor = atom.workspace.getActiveTextEditor() 20 | editor.setText('// "http://github.com"') 21 | 22 | spyOn(shell, 'openExternal') 23 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 24 | expect(shell.openExternal).not.toHaveBeenCalled() 25 | 26 | editor.setCursorBufferPosition([0, 4]) 27 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 28 | 29 | expect(shell.openExternal).toHaveBeenCalled() 30 | expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') 31 | 32 | shell.openExternal.reset() 33 | editor.setCursorBufferPosition([0, 8]) 34 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 35 | 36 | expect(shell.openExternal).toHaveBeenCalled() 37 | expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') 38 | 39 | shell.openExternal.reset() 40 | editor.setCursorBufferPosition([0, 21]) 41 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 42 | 43 | expect(shell.openExternal).toHaveBeenCalled() 44 | expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') 45 | }) 46 | 47 | // only works in Atom >= 1.32.0 48 | // https://github.com/atom/link/pull/33#issuecomment-419643655 49 | const atomVersion = atom.getVersion().split('.') 50 | if (+atomVersion[0] > 1 || +atomVersion[1] >= 32) { 51 | it("opens an 'atom:' link", async () => { 52 | await atom.workspace.open('sample.md') 53 | 54 | const editor = atom.workspace.getActiveTextEditor() 55 | editor.setText('// "atom://core/open/file?filename=sample.js&line=1&column=2"') 56 | 57 | spyOn(shell, 'openExternal') 58 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 59 | expect(shell.openExternal).not.toHaveBeenCalled() 60 | 61 | editor.setCursorBufferPosition([0, 4]) 62 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 63 | 64 | expect(shell.openExternal).toHaveBeenCalled() 65 | expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') 66 | 67 | shell.openExternal.reset() 68 | editor.setCursorBufferPosition([0, 8]) 69 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 70 | 71 | expect(shell.openExternal).toHaveBeenCalled() 72 | expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') 73 | 74 | shell.openExternal.reset() 75 | editor.setCursorBufferPosition([0, 60]) 76 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 77 | 78 | expect(shell.openExternal).toHaveBeenCalled() 79 | expect(shell.openExternal.argsForCall[0][0]).toBe('atom://core/open/file?filename=sample.js&line=1&column=2') 80 | }) 81 | } 82 | 83 | describe('when the cursor is on a [name][url-name] style markdown link', () => 84 | it('opens the named url', async () => { 85 | await atom.workspace.open('README.md') 86 | 87 | const editor = atom.workspace.getActiveTextEditor() 88 | editor.setText(`\ 89 | you should [click][here] 90 | you should not [click][her] 91 | 92 | [here]: http://github.com\ 93 | ` 94 | ) 95 | 96 | spyOn(shell, 'openExternal') 97 | editor.setCursorBufferPosition([0, 0]) 98 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 99 | expect(shell.openExternal).not.toHaveBeenCalled() 100 | 101 | editor.setCursorBufferPosition([0, 20]) 102 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 103 | 104 | expect(shell.openExternal).toHaveBeenCalled() 105 | expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') 106 | 107 | shell.openExternal.reset() 108 | editor.setCursorBufferPosition([1, 24]) 109 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 110 | 111 | expect(shell.openExternal).not.toHaveBeenCalled() 112 | }) 113 | ) 114 | 115 | it('does not open non http/https/atom links', async () => { 116 | await atom.workspace.open('sample.md') 117 | 118 | const editor = atom.workspace.getActiveTextEditor() 119 | editor.setText('// ftp://github.com\n') 120 | 121 | spyOn(shell, 'openExternal') 122 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 123 | expect(shell.openExternal).not.toHaveBeenCalled() 124 | 125 | editor.setCursorBufferPosition([0, 5]) 126 | atom.commands.dispatch(atom.views.getView(editor), 'link:open') 127 | 128 | expect(shell.openExternal).not.toHaveBeenCalled() 129 | }) 130 | }) 131 | }) 132 | --------------------------------------------------------------------------------