diff --git a/.coffeelintignore b/.coffeelintignore deleted file mode 100644 index 1db51fe..0000000 --- a/.coffeelintignore +++ /dev/null @@ -1 +0,0 @@ -spec/fixtures diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index a5dd715..0000000 --- a/coffeelint.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "max_line_length": { - "level": "ignore" - }, - "no_empty_param_list": { - "level": "error" - }, - "arrow_spacing": { - "level": "error" - }, - "no_interpolation_in_single_quotes": { - "level": "error" - }, - "no_debugger": { - "level": "error" - }, - "prefer_english_operator": { - "level": "error" - }, - "colon_assignment_spacing": { - "spacing": { - "left": 0, - "right": 1 - }, - "level": "error" - }, - "braces_spacing": { - "spaces": 0, - "level": "error" - }, - "spacing_after_comma": { - "level": "error" - }, - "no_stand_alone_at": { - "level": "error" - } -} diff --git a/lib/link.coffee b/lib/link.coffee deleted file mode 100644 index f6c9b01..0000000 --- a/lib/link.coffee +++ /dev/null @@ -1,65 +0,0 @@ -url = require 'url' -{shell} = require 'electron' -_ = require 'underscore-plus' - -selector = null - -module.exports = - activate: -> - atom.commands.add('atom-workspace', 'link:open', openLink) - -openLink = -> - editor = atom.workspace.getActiveTextEditor() - return unless editor? - - link = linkUnderCursor(editor) - return unless link? - - if editor.getGrammar().scopeName is 'source.gfm' - link = linkForName(editor.getBuffer(), link) - - {protocol} = url.parse(link) - if protocol is 'http:' or protocol is 'https:' - shell.openExternal(link) - -# Get the link under the cursor in the editor -# -# Returns a {String} link or undefined if no link found. -linkUnderCursor = (editor) -> - cursorPosition = editor.getCursorBufferPosition() - link = linkAtPosition(editor, cursorPosition) - return link if link? - - # Look for a link to the left of the cursor - if cursorPosition.column > 0 - 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) -> - unless selector? - {ScopeSelector} = require 'first-mate' - selector = new ScopeSelector('markup.underline.link') - - if token = editor.tokenForBufferPosition(bufferPosition) - token.value if token.value and selector.matches(token.scopes) - -# 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 = (buffer, linkName) -> - link = linkName - regex = new RegExp("^\\s*\\[#{_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$", 'g') - buffer.backwardsScanInRange regex, buffer.getRange(), ({match, stop}) -> - link = match[1] - stop() - link diff --git a/lib/link.js b/lib/link.js new file mode 100644 index 0000000..3e04ddf --- /dev/null +++ b/lib/link.js @@ -0,0 +1,74 @@ +const url = require('url') +const {shell} = require('electron') +const _ = require('underscore-plus') + +let selector = null + +module.exports = { + activate () { + atom.commands.add('atom-workspace', 'link:open', () => this.openLink()) + }, + + 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.getBuffer(), link) + } + + const {protocol} = url.parse(link) + if (protocol === 'http:' || protocol === 'https:') 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) { + if (selector == null) { + const {ScopeSelector} = require('first-mate') + selector = new ScopeSelector('markup.underline.link') + } + + const token = editor.tokenForBufferPosition(bufferPosition) + if (token && token.value && selector.matches(token.scopes)) 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 (buffer, linkName) { + let link = linkName + const regex = new RegExp(`^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`, 'g') + buffer.backwardsScanInRange(regex, buffer.getRange(), ({match, stop}) => { + link = match[1] + stop() + }) + return link + } +} diff --git a/package.json b/package.json index da30d31..d557c39 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,17 @@ "underscore-plus": "1.x" }, "devDependencies": { - "coffeelint": "^1.9.7" + "standard": "^10.0.3" + }, + "standard": { + "env": { + "atomtest": true, + "browser": true, + "jasmine": true, + "node": true + }, + "globals": [ + "atom" + ] } } diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js new file mode 100644 index 0000000..73002c0 --- /dev/null +++ b/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/spec/link-spec.coffee b/spec/link-spec.coffee deleted file mode 100644 index ebabcf1..0000000 --- a/spec/link-spec.coffee +++ /dev/null @@ -1,98 +0,0 @@ -{shell} = require 'electron' - -describe "link package", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-gfm') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - waitsForPromise -> - activationPromise = atom.packages.activatePackage('link') - atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open') - activationPromise - - describe "when the cursor is on a link", -> - it "opens the link using the 'open' command", -> - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - 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' - - describe "when the cursor is on a [name][url-name] style markdown link", -> - it "opens the named url", -> - waitsForPromise -> - atom.workspace.open('README.md') - - runs -> - 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 links", -> - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - 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() diff --git a/spec/link-spec.js b/spec/link-spec.js new file mode 100644 index 0000000..36830aa --- /dev/null +++ b/spec/link-spec.js @@ -0,0 +1,96 @@ +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-javascript') + 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.js') + + 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') + }) + + 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 links', async () => { + await atom.workspace.open('sample.js') + + 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() + }) + }) +})