Merge branch 'master' into ns-mb-detangle-editor

This commit is contained in:
Max Brunsfeld
2016-08-02 14:12:18 -07:00
22 changed files with 544 additions and 668 deletions

View File

@@ -20,6 +20,7 @@ These are just guidelines, not rules, use your best judgment and feel free to pr
[Styleguides](#styleguides)
* [Git Commit Messages](#git-commit-messages)
* [JavaScript Styleguide](#javascript-styleguide)
* [CoffeeScript Styleguide](#coffeescript-styleguide)
* [Specs Styleguide](#specs-styleguide)
* [Documentation Styleguide](#documentation-styleguide)
@@ -280,6 +281,25 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F
* :arrow_down: `:arrow_down:` when downgrading dependencies
* :shirt: `:shirt:` when removing linter warnings
### JavaScript Styleguide
All JavaScript must adhere to [JavaScript Standard Style](http://standardjs.com/).
* Prefer `Object.assign()` to the object spread operator (`{...anotherObj}`)
* Inline `export`s with expressions
```js
// Use this:
export default class ClassName {
}
// Instead of:
class ClassName {
}
export default ClassName
```
### CoffeeScript Styleguide
* Set parameter defaults without spaces around the equal sign

View File

@@ -320,7 +320,7 @@ getDefaultChannelAndReleaseBranch = (version) ->
else
channel = 'stable'
minorVersion = version.match(/^\d\.\d/)[0]
minorVersion = version.match(/^\d+\.\d+/)[0]
releaseBranch = "#{minorVersion}-releases"
[channel, releaseBranch]

View File

@@ -30,6 +30,7 @@
"grunt-peg": "~1.1.0",
"grunt-shell": "~0.3.1",
"grunt-standard": "^2.0.0",
"joanna": "0.0.6",
"legal-eagle": "~0.13.0",
"minidump": "~0.9",
"npm": "3.10.5",

View File

@@ -4,7 +4,9 @@ fs = require 'fs-plus'
_ = require 'underscore-plus'
donna = require 'donna'
joanna = require 'joanna'
tello = require 'tello'
glob = require 'glob'
module.exports = (grunt) ->
getClassesToInclude = ->
@@ -30,8 +32,16 @@ module.exports = (grunt) ->
grunt.registerTask 'build-docs', 'Builds the API docs in src', ->
docsOutputDir = grunt.config.get('docsOutputDir')
metadata = donna.generateMetadata(['.'])
api = tello.digest(metadata)
[coffeeMetadata] = donna.generateMetadata(['.'])
jsMetadata = joanna(glob.sync('src/*.js'))
metadata = {
repository: coffeeMetadata.repository,
version: coffeeMetadata.version,
files: Object.assign(coffeeMetadata.files, jsMetadata.files)
}
api = tello.digest([metadata])
_.extend(api.classes, getClassesToInclude())
api.classes = sortClasses(api.classes)

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.10.0-dev",
"version": "1.11.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@@ -58,7 +58,7 @@
"sinon": "1.17.4",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "9.2.2",
"text-buffer": "9.2.3",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
@@ -77,7 +77,7 @@
"one-light-syntax": "1.3.0",
"solarized-dark-syntax": "1.0.2",
"solarized-light-syntax": "1.0.2",
"about": "1.5.3",
"about": "1.6.0",
"archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
"autocomplete-css": "0.11.2",
@@ -109,7 +109,7 @@
"notifications": "0.65.0",
"open-on-github": "1.2.0",
"package-generator": "1.0.0",
"settings-view": "0.241.0",
"settings-view": "0.241.1",
"snippets": "1.0.2",
"spell-check": "0.67.1",
"status-bar": "1.4.0",

View File

@@ -24,14 +24,14 @@ describe "Package", ->
mockLocalStorage()
it "does not activate it", ->
packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module')
packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module')
pack = buildPackage(packagePath)
expect(pack.isCompatible()).toBe false
expect(pack.incompatibleModules[0].name).toBe 'native-module'
expect(pack.incompatibleModules[0].path).toBe path.join(packagePath, 'node_modules', 'native-module')
it "utilizes _atomModuleCache if present to determine the package's native dependencies", ->
packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-ignored-incompatible-native-module')
packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-ignored-incompatible-native-module')
pack = buildPackage(packagePath)
expect(pack.getNativeModuleDependencyPaths().length).toBe(1) # doesn't see the incompatible module
expect(pack.isCompatible()).toBe true
@@ -41,8 +41,7 @@ describe "Package", ->
expect(pack.isCompatible()).toBe false
it "caches the incompatible native modules in local storage", ->
packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module')
packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module')
expect(buildPackage(packagePath).isCompatible()).toBe false
expect(global.localStorage.getItem.callCount).toBe 1
expect(global.localStorage.setItem.callCount).toBe 1
@@ -51,6 +50,18 @@ describe "Package", ->
expect(global.localStorage.getItem.callCount).toBe 2
expect(global.localStorage.setItem.callCount).toBe 1
it "logs an error to the console describing the problem", ->
packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module')
spyOn(console, 'warn')
spyOn(atom.notifications, 'addFatalError')
buildPackage(packagePath).activateNow()
expect(atom.notifications.addFatalError).not.toHaveBeenCalled()
expect(console.warn.callCount).toBe(1)
expect(console.warn.mostRecentCall.args[0]).toContain('it requires one or more incompatible native modules (native-module)')
describe "::rebuild()", ->
beforeEach ->
mockLocalStorage()

View File

@@ -1,91 +0,0 @@
{times, random} = require 'underscore-plus'
randomWords = require 'random-words'
TextBuffer = require 'text-buffer'
TextEditor = require '../src/text-editor'
describe "TextEditor", ->
[editor, tokenizedBuffer, buffer, steps] = []
softWrapColumn = 80
beforeEach ->
atom.config.set('editor.softWrapAtPreferredLineLength', true)
atom.config.set('editor.preferredLineLength', softWrapColumn)
it "properly renders soft-wrapped lines when randomly mutated", ->
times 10, (i) ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->
randomlyMutateEditor()
verifyLines()
verifyLines = ->
{bufferRows, screenLines} = getReferenceScreenLines()
for referenceBufferRow, screenRow in bufferRows
referenceScreenLine = screenLines[screenRow]
actualBufferRow = editor.bufferRowForScreenRow(screenRow)
unless actualBufferRow is referenceBufferRow
logLines()
throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", )
actualScreenLine = editor.lineTextForScreenRow(screenRow)
unless actualScreenLine is referenceScreenLine
logLines()
throw new Error("Invalid line text at screen row #{screenRow}")
logLines = ->
console.log "==== screen lines ===="
editor.logScreenLines()
console.log "==== reference lines ===="
{bufferRows, screenLines} = getReferenceScreenLines()
for bufferRow, screenRow in bufferRows
console.log screenRow, bufferRow, screenLines[screenRow].text
console.log "==== steps to reproduce this failure: ==="
for step in steps
console.log 'editor.' + step[0] + '('+ step[1..].map((a) -> JSON.stringify(a)).join(', ') + ')'
randomlyMutateEditor = ->
if Math.random() < .2
softWrapped = not editor.isSoftWrapped()
steps.push(['setSoftWrapped', softWrapped])
editor.setSoftWrapped(softWrapped)
else
range = getRandomRange()
text = getRandomText()
steps.push(['setTextInBufferRange', range, text])
editor.setTextInBufferRange(range, text)
getRandomRange = ->
startRow = random(0, buffer.getLastRow())
startColumn = random(0, buffer.lineForRow(startRow).length)
endRow = random(startRow, buffer.getLastRow())
endColumn = random(0, buffer.lineForRow(endRow).length)
[[startRow, startColumn], [endRow, endColumn]]
getRandomText = ->
text = []
max = buffer.getText().split(/\s/).length * 0.75
times random(5, max), ->
if Math.random() < .1
text += '\n'
else
text += " " if /\w$/.test(text)
text += randomWords(exactly: 1)
text
getReferenceScreenLines = ->
referenceEditor = atom.workspace.buildTextEditor()
referenceEditor.setEditorWidthInChars(80)
referenceEditor.setText(editor.getText())
referenceEditor.setSoftWrapped(editor.isSoftWrapped())
screenLines = [0..referenceEditor.getLastScreenRow()].map (row) -> referenceEditor.lineTextForScreenRow(row)
bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow())
{screenLines, bufferRows}

View File

@@ -1,104 +0,0 @@
RowMap = require '../src/row-map'
describe "RowMap", ->
map = null
beforeEach ->
map = new RowMap
describe "::screenRowRangeForBufferRow(bufferRow)", ->
it "returns the range of screen rows corresponding to the given buffer row", ->
map.spliceRegions(0, 0, [
{bufferRows: 5, screenRows: 5}
{bufferRows: 1, screenRows: 5}
{bufferRows: 5, screenRows: 5}
{bufferRows: 5, screenRows: 1}
])
expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1]
expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 10]
expect(map.screenRowRangeForBufferRow(6)).toEqual [10, 11]
expect(map.screenRowRangeForBufferRow(11)).toEqual [15, 16]
expect(map.screenRowRangeForBufferRow(12)).toEqual [15, 16]
expect(map.screenRowRangeForBufferRow(16)).toEqual [16, 17]
describe "::bufferRowRangeForScreenRow(screenRow)", ->
it "returns the range of buffer rows corresponding to the given screen row", ->
map.spliceRegions(0, 0, [
{bufferRows: 5, screenRows: 5}
{bufferRows: 1, screenRows: 5}
{bufferRows: 5, screenRows: 5}
{bufferRows: 5, screenRows: 1}
])
expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1]
expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 6]
expect(map.bufferRowRangeForScreenRow(6)).toEqual [5, 6]
expect(map.bufferRowRangeForScreenRow(10)).toEqual [6, 7]
expect(map.bufferRowRangeForScreenRow(14)).toEqual [10, 11]
expect(map.bufferRowRangeForScreenRow(15)).toEqual [11, 16]
expect(map.bufferRowRangeForScreenRow(16)).toEqual [16, 17]
describe "::spliceRegions(startBufferRow, bufferRowCount, regions)", ->
it "can insert regions when empty", ->
regions = [
{bufferRows: 5, screenRows: 5}
{bufferRows: 1, screenRows: 5}
{bufferRows: 5, screenRows: 5}
{bufferRows: 5, screenRows: 1}
]
map.spliceRegions(0, 0, regions)
expect(map.getRegions()).toEqual regions
it "can insert wrapped lines into rectangular regions", ->
map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}])
map.spliceRegions(5, 0, [{bufferRows: 1, screenRows: 3}])
expect(map.getRegions()).toEqual [
{bufferRows: 5, screenRows: 5}
{bufferRows: 1, screenRows: 3}
{bufferRows: 5, screenRows: 5}
]
it "can splice wrapped lines into rectangular regions", ->
map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}])
map.spliceRegions(5, 1, [{bufferRows: 1, screenRows: 3}])
expect(map.getRegions()).toEqual [
{bufferRows: 5, screenRows: 5}
{bufferRows: 1, screenRows: 3}
{bufferRows: 4, screenRows: 4}
]
it "can splice folded lines into rectangular regions", ->
map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}])
map.spliceRegions(5, 3, [{bufferRows: 3, screenRows: 1}])
expect(map.getRegions()).toEqual [
{bufferRows: 5, screenRows: 5}
{bufferRows: 3, screenRows: 1}
{bufferRows: 2, screenRows: 2}
]
it "can replace folded regions with a folded region that surrounds them", ->
map.spliceRegions(0, 0, [
{bufferRows: 3, screenRows: 3}
{bufferRows: 3, screenRows: 1}
{bufferRows: 1, screenRows: 1}
{bufferRows: 3, screenRows: 1}
{bufferRows: 3, screenRows: 3}
])
map.spliceRegions(2, 8, [{bufferRows: 8, screenRows: 1}])
expect(map.getRegions()).toEqual [
{bufferRows: 2, screenRows: 2}
{bufferRows: 8, screenRows: 1}
{bufferRows: 3, screenRows: 3}
]
it "merges adjacent rectangular regions", ->
map.spliceRegions(0, 0, [
{bufferRows: 3, screenRows: 3}
{bufferRows: 3, screenRows: 1}
{bufferRows: 1, screenRows: 1}
{bufferRows: 3, screenRows: 1}
{bufferRows: 3, screenRows: 3}
])
map.spliceRegions(3, 7, [{bufferRows: 5, screenRows: 5}])

View File

@@ -4,100 +4,152 @@ import TokenizedBufferIterator from '../src/tokenized-buffer-iterator'
import {Point} from 'text-buffer'
describe('TokenizedBufferIterator', () => {
it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
const tokenizedBuffer = {
tokenizedLineForRow () {
return {
tags: [-1, -2, -1, -2],
text: '',
openScopes: []
}
},
grammar: {
scopeForId () {
return 'foo'
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
if (row === 0) {
describe('seek(position)', function () {
it('seeks to the leftmost tag boundary at the given position, returning the containing tags', function () {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
return {
tags: [-1, 3, -2, -3],
text: 'bar',
tags: [-1, -2, -3, -4, -5, 3, -3, -4, -6],
text: 'foo',
openScopes: []
}
} else if (row === 1) {
return {
tags: [3],
text: 'baz',
openScopes: [-1]
}
} else if (row === 2) {
return {
tags: [-2],
text: '',
openScopes: [-1]
}
}
},
},
grammar: {
scopeForId (id) {
if (id === -2 || id === -1) {
return 'foo'
} else if (id === -3) {
return 'qux'
grammar: {
scopeForId (id) {
return {
'-1': 'foo', '-2': 'foo',
'-3': 'bar', '-4': 'bar',
'-5': 'baz', '-6': 'baz'
}[id]
}
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
expect(iterator.seek(Point(0, 0))).toEqual([])
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['qux'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['bar'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['qux'])
expect(iterator.getOpenTags()).toEqual([])
expect(iterator.seek(Point(0, 1))).toEqual(['baz'])
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual([])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['bar'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(2, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
expect(iterator.seek(Point(0, 3))).toEqual(['baz'])
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['bar'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['bar', 'baz'])
expect(iterator.getOpenTags()).toEqual([])
})
})
describe('moveToSuccessor()', function () {
it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
const tokenizedBuffer = {
tokenizedLineForRow () {
return {
tags: [-1, -2, -1, -2],
text: '',
openScopes: []
}
},
grammar: {
scopeForId () {
return 'foo'
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
if (row === 0) {
return {
tags: [-1, 3, -2, -3],
text: 'bar',
openScopes: []
}
} else if (row === 1) {
return {
tags: [3],
text: 'baz',
openScopes: [-1]
}
} else if (row === 2) {
return {
tags: [-2],
text: '',
openScopes: [-1]
}
}
},
grammar: {
scopeForId (id) {
if (id === -2 || id === -1) {
return 'foo'
} else if (id === -3) {
return 'qux'
}
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['qux'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['qux'])
expect(iterator.getOpenTags()).toEqual([])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(2, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
})
})

View File

@@ -734,7 +734,7 @@ describe "TokenizedBuffer", ->
expect(iterator.seek(Point(0, 8))).toEqual(["source.js"])
expect(iterator.getPosition()).toEqual(Point(0, 8))
expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"])
expect(iterator.getPosition()).toEqual(Point(1, 5))
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"])
expect(iterator.getPosition()).toEqual(Point(1, 18))

View File

@@ -1,61 +0,0 @@
crypto = require 'crypto'
clipboard = require './safe-clipboard'
# Extended: Represents the clipboard used for copying and pasting in Atom.
#
# An instance of this class is always available as the `atom.clipboard` global.
#
# ## Examples
#
# ```coffee
# atom.clipboard.write('hello')
#
# console.log(atom.clipboard.read()) # 'hello'
# ```
module.exports =
class Clipboard
constructor: ->
@reset()
reset: ->
@metadata = null
@signatureForMetadata = null
# Creates an `md5` hash of some text.
#
# * `text` A {String} to hash.
#
# Returns a hashed {String}.
md5: (text) ->
crypto.createHash('md5').update(text, 'utf8').digest('hex')
# Public: Write the given text to the clipboard.
#
# The metadata associated with the text is available by calling
# {::readWithMetadata}.
#
# * `text` The {String} to store.
# * `metadata` (optional) The additional info to associate with the text.
write: (text, metadata) ->
@signatureForMetadata = @md5(text)
@metadata = metadata
clipboard.writeText(text)
# Public: Read the text from the clipboard.
#
# Returns a {String}.
read: ->
clipboard.readText()
# Public: Read the text from the clipboard and return both the text and the
# associated metadata.
#
# Returns an {Object} with the following keys:
# * `text` The {String} clipboard text.
# * `metadata` The metadata stored by an earlier call to {::write}.
readWithMetadata: ->
text = @read()
if @signatureForMetadata is @md5(text)
{text, @metadata}
else
{text}

70
src/clipboard.js Normal file
View File

@@ -0,0 +1,70 @@
/** @babel */
import crypto from 'crypto'
import clipboard from './safe-clipboard'
// Extended: Represents the clipboard used for copying and pasting in Atom.
//
// An instance of this class is always available as the `atom.clipboard` global.
//
// ## Examples
//
// ```coffee
// atom.clipboard.write('hello')
//
// console.log(atom.clipboard.read()) # 'hello'
// ```
export default class Clipboard {
constructor () {
this.reset()
}
reset () {
this.metadata = null
this.signatureForMetadata = null
}
// Creates an `md5` hash of some text.
//
// * `text` A {String} to hash.
//
// Returns a hashed {String}.
md5 (text) {
return crypto.createHash('md5').update(text, 'utf8').digest('hex')
}
// Public: Write the given text to the clipboard.
//
// The metadata associated with the text is available by calling
// {::readWithMetadata}.
//
// * `text` The {String} to store.
// * `metadata` (optional) The additional info to associate with the text.
write (text, metadata) {
this.signatureForMetadata = this.md5(text)
this.metadata = metadata
clipboard.writeText(text)
}
// Public: Read the text from the clipboard.
//
// Returns a {String}.
read () {
return clipboard.readText()
}
// Public: Read the text from the clipboard and return both the text and the
// associated metadata.
//
// Returns an {Object} with the following keys:
// * `text` The {String} clipboard text.
// * `metadata` The metadata stored by an earlier call to {::write}.
readWithMetadata () {
let text = this.read()
if (this.signatureForMetadata === this.md5(text)) {
return {text, metadata: this.metadata}
} else {
return {text}
}
}
}

View File

@@ -1,89 +0,0 @@
_ = require 'underscore-plus'
ParsedColor = null
# Essential: A simple color class returned from {Config::get} when the value
# at the key path is of type 'color'.
module.exports =
class Color
# Essential: Parse a {String} or {Object} into a {Color}.
#
# * `value` A {String} such as `'white'`, `#ff00ff`, or
# `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
# and `alpha` properties.
#
# Returns a {Color} or `null` if it cannot be parsed.
@parse: (value) ->
return null if _.isArray(value) or _.isFunction(value)
return null unless _.isObject(value) or _.isString(value)
ParsedColor ?= require 'color'
try
parsedColor = new ParsedColor(value)
catch error
return null
new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
constructor: (red, green, blue, alpha) ->
Object.defineProperties this,
red:
set: (newRed) -> red = parseColor(newRed)
get: -> red
enumerable: true
configurable: false
green:
set: (newGreen) -> green = parseColor(newGreen)
get: -> green
enumerable: true
configurable: false
blue:
set: (newBlue) -> blue = parseColor(newBlue)
get: -> blue
enumerable: true
configurable: false
alpha:
set: (newAlpha) -> alpha = parseAlpha(newAlpha)
get: -> alpha
enumerable: true
configurable: false
@red = red
@green = green
@blue = blue
@alpha = alpha
# Essential: Returns a {String} in the form `'#abcdef'`.
toHexString: ->
"##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}"
# Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
toRGBAString: ->
"rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})"
isEqual: (color) ->
return true if this is color
color = Color.parse(color) unless color instanceof Color
return false unless color?
color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha
clone: -> new Color(@red, @green, @blue, @alpha)
parseColor = (color) ->
color = parseInt(color)
color = 0 if isNaN(color)
color = Math.max(color, 0)
color = Math.min(color, 255)
color
parseAlpha = (alpha) ->
alpha = parseFloat(alpha)
alpha = 1 if isNaN(alpha)
alpha = Math.max(alpha, 0)
alpha = Math.min(alpha, 1)
alpha
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 16
hex

134
src/color.js Normal file
View File

@@ -0,0 +1,134 @@
/** @babel */
let ParsedColor = null
// Essential: A simple color class returned from {Config::get} when the value
// at the key path is of type 'color'.
export default class Color {
// Essential: Parse a {String} or {Object} into a {Color}.
//
// * `value` A {String} such as `'white'`, `#ff00ff`, or
// `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
// and `alpha` properties.
//
// Returns a {Color} or `null` if it cannot be parsed.
static parse (value) {
switch (typeof value) {
case 'string':
break
case 'object':
if (Array.isArray(value)) { return null }
break
default:
return null
}
if (!ParsedColor) {
ParsedColor = require('color')
}
try {
var parsedColor = new ParsedColor(value)
} catch (error) {
return null
}
return new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
}
constructor (red, green, blue, alpha) {
this.red = red
this.green = green
this.blue = blue
this.alpha = alpha
}
set red (red) {
this._red = parseColor(red)
}
set green (green) {
this._green = parseColor(green)
}
set blue (blue) {
this._blue = parseColor(blue)
}
set alpha (alpha) {
this._alpha = parseAlpha(alpha)
}
get red () {
return this._red
}
get green () {
return this._green
}
get blue () {
return this._blue
}
get alpha () {
return this._alpha
}
// Essential: Returns a {String} in the form `'#abcdef'`.
toHexString () {
return `#${numberToHexString(this.red)}${numberToHexString(this.green)}${numberToHexString(this.blue)}`
}
// Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
toRGBAString () {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`
}
isEqual (color) {
if (this === color) {
return true
}
if (!(color instanceof Color)) {
color = Color.parse(color)
}
if (color == null) {
return false
}
return color.red === this.red && color.blue === this.blue && color.green === this.green && color.alpha === this.alpha
}
clone () {
return new Color(this.red, this.green, this.blue, this.alpha)
}
}
function parseColor (colorString) {
const color = parseInt(colorString, 10)
if (isNaN(color)) {
return 0
} else {
return Math.min(Math.max(color, 0), 255)
}
}
function parseAlpha (alphaString) {
const alpha = parseFloat(alphaString)
if (isNaN(alpha)) {
return 1
} else {
return Math.min(Math.max(alpha, 0), 1)
}
}
function numberToHexString (number) {
const hex = number.toString(16)
if (number < 16) {
return `0${hex}`
} else {
return hex
}
}

View File

@@ -1,68 +0,0 @@
{Disposable} = require 'event-kit'
# Extended: Manages the deserializers used for serialized state
#
# An instance of this class is always available as the `atom.deserializers`
# global.
#
# ## Examples
#
# ```coffee
# class MyPackageView extends View
# atom.deserializers.add(this)
#
# @deserialize: (state) ->
# new MyPackageView(state)
#
# constructor: (@state) ->
#
# serialize: ->
# @state
# ```
module.exports =
class DeserializerManager
constructor: (@atomEnvironment) ->
@deserializers = {}
# Public: Register the given class(es) as deserializers.
#
# * `deserializers` One or more deserializers to register. A deserializer can
# be any object with a `.name` property and a `.deserialize()` method. A
# common approach is to register a *constructor* as the deserializer for its
# instances by adding a `.deserialize()` class method. When your method is
# called, it will be passed serialized state as the first argument and the
# {Atom} environment object as the second argument, which is useful if you
# wish to avoid referencing the `atom` global.
add: (deserializers...) ->
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
new Disposable =>
delete @deserializers[deserializer.name] for deserializer in deserializers
return
getDeserializerCount: ->
Object.keys(@deserializers).length
# Public: Deserialize the state and params.
#
# * `state` The state {Object} to deserialize.
deserialize: (state) ->
return unless state?
if deserializer = @get(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
deserializer.deserialize(state, @atomEnvironment)
else
console.warn "No deserializer found for", state
# Get the deserializer for the state.
#
# * `state` The state {Object} being deserialized.
get: (state) ->
return unless state?
name = state.get?('deserializer') ? state.deserializer
@deserializers[name]
clear: ->
@deserializers = {}

100
src/deserializer-manager.js Normal file
View File

@@ -0,0 +1,100 @@
/** @babel */
import {Disposable} from 'event-kit'
// Extended: Manages the deserializers used for serialized state
//
// An instance of this class is always available as the `atom.deserializers`
// global.
//
// ## Examples
//
// ```coffee
// class MyPackageView extends View
// atom.deserializers.add(this)
//
// @deserialize: (state) ->
// new MyPackageView(state)
//
// constructor: (@state) ->
//
// serialize: ->
// @state
// ```
export default class DeserializerManager {
constructor (atomEnvironment) {
this.atomEnvironment = atomEnvironment
this.deserializers = {}
}
// Public: Register the given class(es) as deserializers.
//
// * `deserializers` One or more deserializers to register. A deserializer can
// be any object with a `.name` property and a `.deserialize()` method. A
// common approach is to register a *constructor* as the deserializer for its
// instances by adding a `.deserialize()` class method. When your method is
// called, it will be passed serialized state as the first argument and the
// {Atom} environment object as the second argument, which is useful if you
// wish to avoid referencing the `atom` global.
add (...deserializers) {
for (let i = 0; i < deserializers.length; i++) {
let deserializer = deserializers[i]
this.deserializers[deserializer.name] = deserializer
}
return new Disposable(() => {
for (let j = 0; j < deserializers.length; j++) {
let deserializer = deserializers[j]
delete this.deserializers[deserializer.name]
}
})
}
getDeserializerCount () {
return Object.keys(this.deserializers).length
}
// Public: Deserialize the state and params.
//
// * `state` The state {Object} to deserialize.
deserialize (state) {
if (state == null) {
return
}
const deserializer = this.get(state)
if (deserializer) {
let stateVersion = (
(typeof state.get === 'function') && state.get('version') ||
state.version
)
if ((deserializer.version != null) && deserializer.version !== stateVersion) {
return
}
return deserializer.deserialize(state, this.atomEnvironment)
} else {
return console.warn('No deserializer found for', state)
}
}
// Get the deserializer for the state.
//
// * `state` The state {Object} being deserialized.
get (state) {
if (state == null) {
return
}
let stateDeserializer = (
(typeof state.get === 'function') && state.get('deserializer') ||
state.deserializer
)
return this.deserializers[stateDeserializer]
}
clear () {
this.deserializers = {}
}
}

View File

@@ -3,7 +3,8 @@ Path = require 'path'
exeName = Path.basename(process.execPath)
appPath = "\"#{process.execPath}\""
appName = exeName.replace('atom', 'Atom').replace('beta', 'Beta').replace('.exe', '')
isBeta = appPath.includes(' Beta')
appName = exeName.replace('atom', (if isBeta then 'Atom Beta' else 'Atom' )).replace('.exe', '')
class ShellOption
constructor: (key, parts) ->
@@ -13,11 +14,11 @@ class ShellOption
isRegistered: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
callback(not err? and val.value is @parts[0].value)
callback(not err? and val? and val.value is @parts[0].value)
register: (callback) =>
doneCount = @parts.length
for part in @parts
@parts.forEach (part) =>
reg = new Registry({hive: 'HKCU', key: if part.key? then "#{@key}\\#{part.key}" else @key})
reg.create( -> reg.set part.name, Registry.REG_SZ, part.value, -> callback() if --doneCount is 0)
@@ -31,7 +32,7 @@ class ShellOption
update: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
if err? or not val.value.includes '\\' + exeName
if err? or not val? or val.value.includes '\\' + exeName
callback(err)
else
@register callback

View File

@@ -427,7 +427,7 @@ class Package
return @mainModule if @mainModuleRequired
unless @isCompatible()
console.warn """
Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.map(@incompatibleModules, 'name').join(', ')}).
Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}).
Run `apm rebuild` in the package directory and restart Atom to resolve.
"""
return

View File

@@ -1,120 +0,0 @@
{spliceWithArray} = require 'underscore-plus'
# Used by the display buffer to map screen rows to buffer rows and vice-versa.
# This mapping may not be 1:1 due to folds and soft-wraps. This object maintains
# an array of regions, which contain `bufferRows` and `screenRows` fields.
#
# Rectangular Regions:
# If a region has the same number of buffer rows and screen rows, it is referred
# to as "rectangular", and represents one or more non-soft-wrapped, non-folded
# lines.
#
# Trapezoidal Regions:
# If a region has one buffer row and more than one screen row, it represents a
# soft-wrapped line. If a region has one screen row and more than one buffer
# row, it represents folded lines
module.exports =
class RowMap
constructor: ->
@regions = []
# Public: Returns a copy of all the regions in the map
getRegions: ->
@regions.slice()
# Public: Returns an end-row-exclusive range of screen rows corresponding to
# the given buffer row. If the buffer row is soft-wrapped, the range may span
# multiple screen rows. Otherwise it will span a single screen row.
screenRowRangeForBufferRow: (targetBufferRow) ->
{region, bufferRows, screenRows} = @traverseToBufferRow(targetBufferRow)
if region? and region.bufferRows isnt region.screenRows
[screenRows, screenRows + region.screenRows]
else
screenRows += targetBufferRow - bufferRows
[screenRows, screenRows + 1]
# Public: Returns an end-row-exclusive range of buffer rows corresponding to
# the given screen row. If the screen row is the first line of a folded range
# of buffer rows, the range may span multiple buffer rows. Otherwise it will
# span a single buffer row.
bufferRowRangeForScreenRow: (targetScreenRow) ->
{region, screenRows, bufferRows} = @traverseToScreenRow(targetScreenRow)
if region? and region.bufferRows isnt region.screenRows
[bufferRows, bufferRows + region.bufferRows]
else
bufferRows += targetScreenRow - screenRows
[bufferRows, bufferRows + 1]
# Public: If the given buffer row is part of a folded row range, returns that
# row range. Otherwise returns a range spanning only the given buffer row.
bufferRowRangeForBufferRow: (targetBufferRow) ->
{region, bufferRows} = @traverseToBufferRow(targetBufferRow)
if region? and region.bufferRows isnt region.screenRows
[bufferRows, bufferRows + region.bufferRows]
else
[targetBufferRow, targetBufferRow + 1]
# Public: Given a starting buffer row, the number of buffer rows to replace,
# and an array of regions of shape {bufferRows: n, screenRows: m}, splices
# the regions at the appropriate location in the map. This method is used by
# display buffer to keep the map updated when the underlying buffer changes.
spliceRegions: (startBufferRow, bufferRowCount, regions) ->
endBufferRow = startBufferRow + bufferRowCount
{index, bufferRows} = @traverseToBufferRow(startBufferRow)
precedingRows = startBufferRow - bufferRows
count = 0
while region = @regions[index + count]
count++
bufferRows += region.bufferRows
if bufferRows >= endBufferRow
followingRows = bufferRows - endBufferRow
break
if precedingRows > 0
regions.unshift({bufferRows: precedingRows, screenRows: precedingRows})
if followingRows > 0
regions.push({bufferRows: followingRows, screenRows: followingRows})
spliceWithArray(@regions, index, count, regions)
@mergeAdjacentRectangularRegions(index - 1, index + regions.length)
traverseToBufferRow: (targetBufferRow) ->
bufferRows = 0
screenRows = 0
for region, index in @regions
if (bufferRows + region.bufferRows) > targetBufferRow
return {region, index, screenRows, bufferRows}
bufferRows += region.bufferRows
screenRows += region.screenRows
{index, screenRows, bufferRows}
traverseToScreenRow: (targetScreenRow) ->
bufferRows = 0
screenRows = 0
for region, index in @regions
if (screenRows + region.screenRows) > targetScreenRow
return {region, index, screenRows, bufferRows}
bufferRows += region.bufferRows
screenRows += region.screenRows
{index, screenRows, bufferRows}
mergeAdjacentRectangularRegions: (startIndex, endIndex) ->
for index in [endIndex..startIndex]
if 0 < index < @regions.length
leftRegion = @regions[index - 1]
rightRegion = @regions[index]
leftIsRectangular = leftRegion.bufferRows is leftRegion.screenRows
rightIsRectangular = rightRegion.bufferRows is rightRegion.screenRows
if leftIsRectangular and rightIsRectangular
@regions.splice index - 1, 2,
bufferRows: leftRegion.bufferRows + rightRegion.bufferRows
screenRows: leftRegion.screenRows + rightRegion.screenRows
return
# Public: Returns an array of strings describing the map's regions.
inspect: ->
for {bufferRows, screenRows} in @regions
"#{bufferRows}:#{screenRows}"

View File

@@ -18,24 +18,31 @@ class TokenizedBufferIterator
@currentLineLength = currentLine.text.length
@containingTags = @currentLineOpenTags.map (id) => @tokenizedBuffer.grammar.scopeForId(id)
currentColumn = 0
for tag, index in @currentTags
if tag >= 0
if currentColumn >= position.column and @isAtTagBoundary()
if currentColumn is position.column
@tagIndex = index
break
else
currentColumn += tag
@containingTags.pop() while @closeTags.shift()
@containingTags.push(tag) while tag = @openTags.shift()
else
scopeName = @tokenizedBuffer.grammar.scopeForId(tag)
if tag % 2 is 0
if @openTags.length > 0
@containingTags.push(openTag) while openTag = @openTags.shift()
if currentColumn > position.column
@tagIndex = index
break
else
@closeTags.push(scopeName)
else
else
scopeName = @tokenizedBuffer.grammar.scopeForId(tag)
if tag % 2 is 0 # close tag
if @openTags.length > 0
if currentColumn is position.column
@tagIndex = index
break
else
@containingTags.pop() while @closeTags.shift()
@containingTags.push(openTag) while openTag = @openTags.shift()
@closeTags.push(scopeName)
else # open tag
@openTags.push(scopeName)
@tagIndex ?= @currentTags.length

View File

@@ -1,27 +0,0 @@
# Public: Measure how long a function takes to run.
#
# description - A {String} description that will be logged to the console when
# the function completes.
# fn - A {Function} to measure the duration of.
#
# Returns the value returned by the given function.
window.measure = (description, fn) ->
start = Date.now()
value = fn()
result = Date.now() - start
console.log description, result
value
# Public: Create a dev tools profile for a function.
#
# description - A {String} description that will be available in the Profiles
# tab of the dev tools.
# fn - A {Function} to profile.
#
# Returns the value returned by the given function.
window.profile = (description, fn) ->
measure description, ->
console.profile(description)
value = fn()
console.profileEnd(description)
value

30
src/window.js Normal file
View File

@@ -0,0 +1,30 @@
// Public: Measure how long a function takes to run.
//
// description - A {String} description that will be logged to the console when
// the function completes.
// fn - A {Function} to measure the duration of.
//
// Returns the value returned by the given function.
window.measure = function (description, fn) {
let start = Date.now()
let value = fn()
let result = Date.now() - start
console.log(description, result)
return value
}
// Public: Create a dev tools profile for a function.
//
// description - A {String} description that will be available in the Profiles
// tab of the dev tools.
// fn - A {Function} to profile.
//
// Returns the value returned by the given function.
window.profile = function (description, fn) {
window.measure(description, function () {
console.profile(description)
let value = fn()
console.profileEnd(description)
return value
})
}