Merge pull request #18105 from atom/migrate-link-package

➡️ Migrate core package 'link' into ./packages
This commit is contained in:
David Wilson
2018-09-26 12:18:10 -07:00
committed by GitHub
13 changed files with 391 additions and 9 deletions

7
package-lock.json generated
View File

@@ -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",
@@ -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"
}

View File

@@ -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",
@@ -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",
@@ -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",

View File

@@ -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] | |

2
packages/link/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
npm-debug.log

1
packages/link/.npmignore Normal file
View File

@@ -0,0 +1 @@
npm-debug.log

20
packages/link/LICENSE.md Normal file
View File

@@ -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.

11
packages/link/README.md Normal file
View File

@@ -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||<kbd>ctrl-shift-o</kbd>||
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.

View File

@@ -0,0 +1,2 @@
'.platform-darwin atom-text-editor':
'ctrl-shift-o': 'link:open'

75
packages/link/lib/link.js Normal file
View File

@@ -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
}
}

View File

@@ -0,0 +1,4 @@
'context-menu':
'atom-text-editor .syntax--markup.syntax--underline.syntax--link': [
{label: 'Open link', command: 'link:open'}
]

View File

@@ -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"
]
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,132 @@
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.33.0
// https://github.com/atom/link/pull/33#issuecomment-419643655
const atomVersion = atom.getVersion().split('.')
console.error("atomVersion", atomVersion)
if (+atomVersion[0] > 1 || +atomVersion[1] >= 33) {
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()
})
})
})