diff --git a/package.json b/package.json index 1c73a86b9..c2e024a22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.183.0", + "version": "0.185.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -93,9 +93,9 @@ "dev-live-reload": "0.41.0", "encoding-selector": "0.18.0", "exception-reporting": "0.24.0", - "find-and-replace": "0.157.0", + "find-and-replace": "0.158.0", "fuzzy-finder": "0.68.0", - "git-diff": "0.53.0", + "git-diff": "0.54.0", "go-to-line": "0.30.0", "grammar-selector": "0.45.0", "image-view": "0.49.0", @@ -116,7 +116,7 @@ "symbols-view": "0.84.0", "tabs": "0.67.0", "timecop": "0.31.0", - "tree-view": "0.161.0", + "tree-view": "0.162.0", "update-package-dependencies": "0.8.0", "welcome": "0.24.0", "whitespace": "0.29.0", @@ -132,14 +132,14 @@ "language-html": "0.29.0", "language-hyperlink": "0.12.2", "language-java": "0.14.0", - "language-javascript": "0.58.0", + "language-javascript": "0.59.0", "language-json": "0.12.0", "language-less": "0.25.0", "language-make": "0.13.0", "language-mustache": "0.11.0", "language-objective-c": "0.15.0", "language-perl": "0.10.0", - "language-php": "0.20.0", + "language-php": "0.21.0", "language-property-list": "0.8.0", "language-python": "0.32.0", "language-ruby": "0.48.0", diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index f9abac930..4aec0ef8d 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -1,7 +1,14 @@ babel = require '../src/babel' crypto = require 'crypto' +grim = require 'grim' describe "Babel transpiler support", -> + beforeEach -> + jasmine.snapshotDeprecations() + + afterEach -> + jasmine.restoreDeprecationsSnapshot() + describe "::createBabelVersionAndOptionsDigest", -> it "returns a digest for the library version and specified options", -> defaultOptions = @@ -30,21 +37,27 @@ describe "Babel transpiler support", -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-single-quotes.js') expect(transpiled(3)).toBe 4 + expect(grim.getDeprecationsLength()).toBe 0 describe "when a .js file starts with 'use 6to5';", -> - it "transpiles it using 6to5", -> + it "transpiles it using babel and adds a pragma deprecation", -> + expect(grim.getDeprecationsLength()).toBe 0 transpiled = require('./fixtures/babel/6to5-single-quotes.js') expect(transpiled(3)).toBe 4 + expect(grim.getDeprecationsLength()).toBe 1 describe 'when a .js file starts with "use babel";', -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-double-quotes.js') expect(transpiled(3)).toBe 4 + expect(grim.getDeprecationsLength()).toBe 0 describe 'when a .js file starts with "use 6to5";', -> - it "transpiles it using babel", -> + it "transpiles it using babel and adds a pragma deprecation", -> + expect(grim.getDeprecationsLength()).toBe 0 transpiled = require('./fixtures/babel/6to5-double-quotes.js') expect(transpiled(3)).toBe 4 + expect(grim.getDeprecationsLength()).toBe 1 describe "when a .js file does not start with 'use 6to6';", -> it "does not transpile it using babel", -> diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 9b3167492..514a51625 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -75,7 +75,7 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' atom.config.set('editor.preferredLineLength', 5) - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe 'funct' + expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' fun' atom.config.set('editor.softWrapAtPreferredLineLength', false) expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' @@ -92,7 +92,7 @@ describe "DisplayBuffer", -> describe "when there is whitespace before the boundary", -> it "wraps the line at the end of the first whitespace preceding the boundary", -> expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' - expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe 'sort(left).concat(pivot).concat(sort(right));' + expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe ' sort(left).concat(pivot).concat(sort(right));' describe "when there is no whitespace before the boundary", -> it "wraps the line exactly at the boundary since there's no more graceful place to wrap it", -> @@ -105,7 +105,7 @@ describe "DisplayBuffer", -> describe "when there is a whitespace character at the max length boundary", -> it "wraps the line at the first non-whitespace character following the boundary", -> expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe 'right = [];' + expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' right = [];' describe "when there are hard tabs", -> beforeEach -> @@ -115,6 +115,11 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[0].isHardTab).toBeTruthy() expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() + describe "when a line is wrapped", -> + it "correctly tokenizes soft wrap indentation tokens", -> + expect(displayBuffer.tokenizedLineForScreenRow(4).tokens[0].isSoftWrapIndentation).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(4).tokens[1].isSoftWrapIndentation).toBeTruthy() + describe "when the buffer changes", -> describe "when buffer lines are updated", -> describe "when whitespace is added after the max line length", -> @@ -138,8 +143,8 @@ describe "DisplayBuffer", -> it "rewraps the line and emits a change event", -> buffer.insert([6, 28], '1234567890') expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? ' - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe 'left1234567890.push(current) : ' - expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe 'right.push(current);' + expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' left1234567890.push(current) : ' + expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe ' right.push(current);' expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' }' expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 1, bufferDelta: 0) @@ -148,7 +153,7 @@ describe "DisplayBuffer", -> it "inserts / updates wrapped lines and emits a change event", -> buffer.insert([6, 21], '1234567890 abcdefghij 1234567890\nabcdefghij') expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot1234567890 abcdefghij ' - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe '1234567890' + expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' 1234567890' expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe 'abcdefghij ? left.push(current) : ' expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe 'right.push(current);' @@ -159,7 +164,7 @@ describe "DisplayBuffer", -> buffer.setTextInRange([[3, 21], [7, 5]], ';') expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items;' expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' return ' - expect(displayBuffer.tokenizedLineForScreenRow(5).text).toBe 'sort(left).concat(pivot).concat(sort(right));' + expect(displayBuffer.tokenizedLineForScreenRow(5).text).toBe ' sort(left).concat(pivot).concat(sort(right));' expect(displayBuffer.tokenizedLineForScreenRow(6).text).toBe ' };' expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 9, screenDelta: -6, bufferDelta: -4) @@ -191,10 +196,10 @@ describe "DisplayBuffer", -> expect(displayBuffer.bufferPositionForScreenPosition([3, 5])).toEqual([3, 5]) expect(displayBuffer.screenPositionForBufferPosition([3, 50])).toEqual([3, 50]) expect(displayBuffer.screenPositionForBufferPosition([3, 51])).toEqual([3, 50]) - expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual([3, 51]) + expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual([3, 50]) expect(displayBuffer.bufferPositionForScreenPosition([3, 50])).toEqual([3, 50]) - expect(displayBuffer.screenPositionForBufferPosition([3, 62])).toEqual([4, 11]) - expect(displayBuffer.bufferPositionForScreenPosition([4, 11])).toEqual([3, 62]) + expect(displayBuffer.screenPositionForBufferPosition([3, 62])).toEqual([4, 15]) + expect(displayBuffer.bufferPositionForScreenPosition([4, 11])).toEqual([3, 58]) # following a wrapped line expect(displayBuffer.screenPositionForBufferPosition([4, 5])).toEqual([5, 5]) @@ -209,9 +214,9 @@ describe "DisplayBuffer", -> describe ".setEditorWidthInChars(length)", -> it "changes the length at which lines are wrapped and emits a change event for all screen lines", -> displayBuffer.setEditorWidthInChars(40) - expect(tokensText displayBuffer.tokenizedLineForScreenRow(4).tokens).toBe 'left = [], right = [];' + expect(tokensText displayBuffer.tokenizedLineForScreenRow(4).tokens).toBe ' left = [], right = [];' expect(tokensText displayBuffer.tokenizedLineForScreenRow(5).tokens).toBe ' while(items.length > 0) {' - expect(tokensText displayBuffer.tokenizedLineForScreenRow(12).tokens).toBe 'sort(left).concat(pivot).concat(sort(rig' + expect(tokensText displayBuffer.tokenizedLineForScreenRow(12).tokens).toBe ' sort(left).concat(pivot).concat(sort' expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 15, screenDelta: 3, bufferDelta: 0) it "only allows positive widths to be assigned", -> @@ -222,7 +227,7 @@ describe "DisplayBuffer", -> it "sets ::scrollLeft to 0 and keeps it there when soft wrapping is enabled", -> displayBuffer.setDefaultCharWidth(10) - displayBuffer.setWidth(50) + displayBuffer.setWidth(85) displayBuffer.manageScrollPosition = true displayBuffer.setSoftWrapped(false) @@ -597,8 +602,8 @@ describe "DisplayBuffer", -> describe "when wrapBeyondNewlines is false (the default)", -> it "wraps positions beyond the end of hard newlines to the end of the line", -> expect(displayBuffer.clipScreenPosition([1, 10000])).toEqual [1, 30] - expect(displayBuffer.clipScreenPosition([4, 30])).toEqual [4, 11] - expect(displayBuffer.clipScreenPosition([4, 1000])).toEqual [4, 11] + expect(displayBuffer.clipScreenPosition([4, 30])).toEqual [4, 15] + expect(displayBuffer.clipScreenPosition([4, 1000])).toEqual [4, 15] describe "when wrapBeyondNewlines is true", -> it "wraps positions past the end of hard newlines to the next line", -> @@ -610,6 +615,18 @@ describe "DisplayBuffer", -> displayBuffer.createFold(3, 5) expect(displayBuffer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0] + describe "when skipSoftWrapIndentation is false (the default)", -> + it "wraps positions at the end of previous soft-wrapped line", -> + expect(displayBuffer.clipScreenPosition([4, 0])).toEqual [3, 50] + expect(displayBuffer.clipScreenPosition([4, 1])).toEqual [3, 50] + expect(displayBuffer.clipScreenPosition([4, 3])).toEqual [3, 50] + + describe "when skipSoftWrapIndentation is true", -> + it "clips positions to the beginning of the line", -> + expect(displayBuffer.clipScreenPosition([4, 0], skipSoftWrapIndentation: true)).toEqual [4, 4] + expect(displayBuffer.clipScreenPosition([4, 1], skipSoftWrapIndentation: true)).toEqual [4, 4] + expect(displayBuffer.clipScreenPosition([4, 3], skipSoftWrapIndentation: true)).toEqual [4, 4] + describe "when wrapAtSoftNewlines is false (the default)", -> it "clips positions at the end of soft-wrapped lines to the character preceding the end of the line", -> expect(displayBuffer.clipScreenPosition([3, 50])).toEqual [3, 50] @@ -620,9 +637,9 @@ describe "DisplayBuffer", -> describe "when wrapAtSoftNewlines is true", -> it "wraps positions at the end of soft-wrapped lines to the next screen line", -> expect(displayBuffer.clipScreenPosition([3, 50], wrapAtSoftNewlines: true)).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([3, 51], wrapAtSoftNewlines: true)).toEqual [4, 0] - expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 0] - expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 0] + expect(displayBuffer.clipScreenPosition([3, 51], wrapAtSoftNewlines: true)).toEqual [4, 4] + expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 4] + expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 4] describe "when skipAtomicTokens is false (the default)", -> it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> @@ -655,8 +672,8 @@ describe "DisplayBuffer", -> buffer.setText('\t\taa bb cc dd ee ff gg') displayBuffer.setSoftWrapped(true) displayBuffer.setEditorWidthInChars(10) - expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 0] - expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 10] + expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 4] + expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 9] describe "::getMaxLineLength()", -> it "returns the length of the longest screen line", -> diff --git a/spec/fixtures/packages/package-with-no-activate/index.js b/spec/fixtures/packages/package-with-no-activate/index.js new file mode 100644 index 000000000..4ba52ba2c --- /dev/null +++ b/spec/fixtures/packages/package-with-no-activate/index.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/spec/fixtures/packages/package-with-no-activate/package.json b/spec/fixtures/packages/package-with-no-activate/package.json new file mode 100644 index 000000000..4be70fd1a --- /dev/null +++ b/spec/fixtures/packages/package-with-no-activate/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-with-no-activate", + "version": "1.0.0" +} diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 3d79b4631..d9898505c 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -220,6 +220,17 @@ describe "PackageManager", -> expect(console.error).not.toHaveBeenCalled() expect(console.warn).not.toHaveBeenCalled() + describe "when the package does not export an activate function", -> + it "activates the package and does not throw an exception or log a warning", -> + spyOn(console, "warn") + expect(-> atom.packages.activatePackage('package-with-no-activate')).not.toThrow() + + waitsFor -> + atom.packages.isPackageActive('package-with-no-activate') + + runs -> + expect(console.warn).not.toHaveBeenCalled() + it "passes the activate method the package's previously serialized state if it exists", -> pack = null waitsForPromise -> diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 5d6e7ed85..d7b9ed2ff 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1171,6 +1171,14 @@ describe "TextEditorComponent", -> regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 2 + it "allows multiple space-delimited decoration classes", -> + decoration.setProperties(type: 'highlight', class: 'foo bar') + nextAnimationFrame() + expect(componentNode.querySelectorAll('.foo.bar').length).toBe 1 + decoration.setProperties(type: 'highlight', class: 'bar baz') + nextAnimationFrame() + expect(componentNode.querySelectorAll('.bar.baz').length).toBe 1 + it "renders classes on the regions directly if 'deprecatedRegionClass' option is defined", -> decoration = editor.decorateMarker(marker, type: 'highlight', class: 'test-highlight', deprecatedRegionClass: 'test-highlight-region') nextAnimationFrame() diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 2947eb06a..c03990bf2 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -287,7 +287,7 @@ describe "TextEditor", -> it "positions the cursor at the buffer position that corresponds to the given screen position", -> editor.setCursorScreenPosition([9, 0]) - expect(editor.getCursorBufferPosition()).toEqual [8, 11] + expect(editor.getCursorBufferPosition()).toEqual [8, 10] describe ".moveUp()", -> it "moves the cursor up", -> @@ -334,6 +334,17 @@ describe "TextEditor", -> expect(editor.getCursors()).toEqual [cursor1] expect(cursor1.getBufferPosition()).toEqual [0,0] + describe "when the cursor was moved down from the beginning of an indented soft-wrapped line", -> + it "moves to the beginning of the previous line", -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual [4, 4] + describe ".moveDown()", -> it "moves the cursor down", -> editor.setCursorScreenPosition([2, 2]) @@ -375,6 +386,16 @@ describe "TextEditor", -> editor.moveUp() expect(editor.getCursorScreenPosition().column).toBe 0 + describe "when the cursor is at the beginning of an indented soft-wrapped line", -> + it "moves to the beginning of the line's continuation on the next screen row", -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual [4, 4] + + describe "when there is a selection", -> beforeEach -> editor.setSelectedBufferRange([[4, 9],[5, 10]]) @@ -432,6 +453,16 @@ describe "TextEditor", -> editor.moveLeft() expect(editor.getCursorScreenPosition()).toEqual [10, 0] + describe "when line is wrapped and follow previous line indentation", -> + beforeEach -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + + it "wraps to the end of the previous line", -> + editor.setCursorScreenPosition([4, 4]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual [3, 50] + describe "when the cursor is on the first line", -> it "remains in the same position (0,0)", -> editor.setCursorScreenPosition(row: 0, column: 0) @@ -628,11 +659,11 @@ describe "TextEditor", -> editor.moveToFirstCharacterOfLine() [cursor1, cursor2] = editor.getCursors() expect(cursor1.getScreenPosition()).toEqual [2,0] - expect(cursor2.getScreenPosition()).toEqual [8,4] + expect(cursor2.getScreenPosition()).toEqual [8,2] editor.moveToFirstCharacterOfLine() expect(cursor1.getScreenPosition()).toEqual [2,0] - expect(cursor2.getScreenPosition()).toEqual [8,0] + expect(cursor2.getScreenPosition()).toEqual [8,2] describe "when soft wrap is off", -> it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> diff --git a/src/babel.coffee b/src/babel.coffee index 766324ce0..c93112e78 100644 --- a/src/babel.coffee +++ b/src/babel.coffee @@ -8,6 +8,7 @@ crypto = require 'crypto' fs = require 'fs-plus' path = require 'path' babel = null # Defer until used +Grim = null # Defer until used stats = hits: 0 @@ -132,10 +133,30 @@ transpile = (sourceCode, filePath, cachePath) -> # either generated on the fly or pulled from cache. loadFile = (module, filePath) -> sourceCode = fs.readFileSync(filePath, 'utf8') - return module._compile(sourceCode, filePath) unless sourceCode.startsWith('"use 6to5"') or - sourceCode.startsWith("'use 6to5'") or - sourceCode.startsWith('"use babel"') or - sourceCode.startsWith("'use babel'") + if sourceCode.startsWith('"use babel"') or sourceCode.startsWith("'use babel'") + # Continue. + else if sourceCode.startsWith('"use 6to5"') or sourceCode.startsWith("'use 6to5'") + # Create a manual deprecation since the stack is too deep to use Grim + # which limits the depth to 3 + Grim ?= require 'grim' + stack = [ + { + fileName: __filename + functionName: 'loadFile' + location: "#{__filename}:161:5" + } + { + fileName: filePath + functionName: '' + location: "#{filePath}:1:1" + } + ] + deprecation = + message: "Use the 'use babel' pragma instead of 'use 6to5'" + stacks: [stack] + Grim.addSerializedDeprecation(deprecation) + else + return module._compile(sourceCode, filePath) cachePath = getCachePath(sourceCode) js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath) diff --git a/src/cursor.coffee b/src/cursor.coffee index bfd6de536..33f50ca9c 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -271,7 +271,7 @@ class Cursor extends Model { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? - @setScreenPosition({row: row - rowCount, column: column}) + @setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true) @goalColumn = column # Public: Moves the cursor down one screen row. @@ -288,7 +288,7 @@ class Cursor extends Model { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? - @setScreenPosition({row: row + rowCount, column: column}) + @setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true) @goalColumn = column # Public: Moves the cursor left one screen column. @@ -359,19 +359,21 @@ class Cursor extends Model # line. moveToFirstCharacterOfLine: -> screenRow = @getScreenRow() - lineBufferRange = @editor.bufferRangeForScreenRange([[screenRow, 0], [screenRow, Infinity]]) + screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true) + screenLineEnd = [screenRow, Infinity] + screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd]) firstCharacterColumn = null - @editor.scanInBufferRange /\S/, lineBufferRange, ({range, stop}) -> + @editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) -> firstCharacterColumn = range.start.column stop() if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn() targetBufferColumn = firstCharacterColumn else - targetBufferColumn = lineBufferRange.start.column + targetBufferColumn = screenLineBufferRange.start.column - @setBufferPosition([lineBufferRange.start.row, targetBufferColumn]) + @setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn]) # Public: Moves the cursor to the end of the line. moveToEndOfScreenLine: -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 9eb7f4a41..79eff09cd 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -819,11 +819,12 @@ class DisplayBuffer extends Model # options - A hash with the following values: # wrapBeyondNewlines: if `true`, continues wrapping past newlines # wrapAtSoftNewlines: if `true`, continues wrapping past soft newlines + # skipSoftWrapIndentation: if `true`, skips soft wrap indentation without wrapping to the previous line # screenLine: if `true`, indicates that you're using a line number, not a row number # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> - { wrapBeyondNewlines, wrapAtSoftNewlines } = options + { wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation } = options { row, column } = Point.fromObject(screenPosition) if row < 0 @@ -841,9 +842,15 @@ class DisplayBuffer extends Model if screenLine.isSoftWrapped() and column >= maxScreenColumn if wrapAtSoftNewlines row++ - column = 0 + column = @screenLines[row].clipScreenColumn(0) else column = screenLine.clipScreenColumn(maxScreenColumn - 1) + else if screenLine.isColumnInsideSoftWrapIndentation(column) + if skipSoftWrapIndentation + column = screenLine.clipScreenColumn(0) + else + row-- + column = @screenLines[row].getMaxScreenColumn() - 1 else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow() row++ column = 0 @@ -851,28 +858,6 @@ class DisplayBuffer extends Model column = screenLine.clipScreenColumn(column, options) new Point(row, column) - # Given a line, finds the point where it would wrap. - # - # line - The {String} to check - # softWrapColumn - The {Number} where you want soft wrapping to occur - # - # Returns a {Number} representing the `line` position where the wrap would take place. - # Returns `null` if a wrap wouldn't occur. - findWrapColumn: (line, softWrapColumn=@getSoftWrapColumn()) -> - return unless @isSoftWrapped() - return unless line.length > softWrapColumn - - if /\s/.test(line[softWrapColumn]) - # search forward for the start of a word past the boundary - for column in [softWrapColumn..line.length] - return column if /\S/.test(line[column]) - return line.length - else - # search backward for the start of the word on the boundary - for column in [softWrapColumn..0] - return column + 1 if /\s/.test(line[column]) - return softWrapColumn - # Calculates a {Range} representing the start of the {TextBuffer} until the end. # # Returns a {Range}. @@ -1157,10 +1142,12 @@ class DisplayBuffer extends Model bufferRow += foldedRowCount else softWraps = 0 - while wrapScreenColumn = @findWrapColumn(tokenizedLine.text) - [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) - screenLines.push(wrappedLine) - softWraps++ + if @isSoftWrapped() + while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumn()) + [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) + break if wrappedLine.hasOnlySoftWrapIndentation() + screenLines.push(wrappedLine) + softWraps++ screenLines.push(tokenizedLine) if softWraps > 0 diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 488a2236f..48d2cc70e 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -1,4 +1,5 @@ RegionStyleProperties = ['top', 'left', 'right', 'width', 'height'] +SpaceRegex = /\s+/ module.exports = class HighlightsComponent @@ -44,8 +45,17 @@ class HighlightsComponent # update class if newHighlightState.class isnt oldHighlightState.class - highlightNode.classList.remove(oldHighlightState.class) if oldHighlightState.class? - highlightNode.classList.add(newHighlightState.class) + if oldHighlightState.class? + if SpaceRegex.test(oldHighlightState.class) + highlightNode.classList.remove(oldHighlightState.class.split(SpaceRegex)...) + else + highlightNode.classList.remove(oldHighlightState.class) + + if SpaceRegex.test(newHighlightState.class) + highlightNode.classList.add(newHighlightState.class.split(SpaceRegex)...) + else + highlightNode.classList.add(newHighlightState.class) + oldHighlightState.class = newHighlightState.class @updateHighlightRegions(id, newHighlightState) diff --git a/src/package.coffee b/src/package.coffee index 6a94133d6..d8ba4a1d8 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -157,7 +157,7 @@ class Package @activateConfig() @activateStylesheets() if @requireMainModule() - @mainModule.activate(atom.packages.getPackageState(@name) ? {}) + @mainModule.activate?(atom.packages.getPackageState(@name) ? {}) @mainActivated = true @activateServices() catch e diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 0502f4987..0de8ecdae 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1817,13 +1817,13 @@ class TextEditor extends Model # Merge cursors that have the same screen position mergeCursors: -> - positions = [] + positions = {} for cursor in @getCursors() position = cursor.getBufferPosition().toString() - if position in positions + if positions.hasOwnProperty(position) cursor.destroy() else - positions.push(position) + positions[position] = true preserveCursorPositionOnBufferReload: -> cursorPosition = null diff --git a/src/token.coffee b/src/token.coffee index 85afe680e..778ea16e6 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -21,7 +21,7 @@ class Token firstTrailingWhitespaceIndex: null hasInvisibleCharacters: false - constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab, @hasPairedCharacter}) -> + constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab, @hasPairedCharacter, @isSoftWrapIndentation}) -> @screenDelta = @value.length @bufferDelta ?= @screenDelta @hasPairedCharacter ?= textUtils.hasPairedCharacter(@value) @@ -144,6 +144,15 @@ class Token isHardTab: isHardTab ) + buildSoftWrapIndentationToken: (length) -> + new Token( + value: _.multiplyString(" ", length), + scopes: @scopes, + bufferDelta: 0, + isAtomic: true, + isSoftWrapIndentation: true + ) + isOnlyWhitespace: -> not WhitespaceRegex.test(@value) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 903a03300..3560c35a1 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -18,6 +18,8 @@ class TokenizedLine @tokens = @breakOutAtomicTokens(tokens) @text = @buildText() @bufferDelta = @buildBufferDelta() + @softWrapIndentationTokens = @getSoftWrapIndentationTokens() + @softWrapIndentationDelta = @buildSoftWrapIndentationDelta() @id = idCounter++ @markLeadingAndTrailingWhitespaceTokens() @@ -49,7 +51,9 @@ class TokenizedLine break if tokenStartColumn + token.screenDelta > column tokenStartColumn += token.screenDelta - if token.isAtomic and tokenStartColumn < column + if @isColumnInsideSoftWrapIndentation(tokenStartColumn) + @softWrapIndentationDelta + else if token.isAtomic and tokenStartColumn < column if skipAtomicTokens tokenStartColumn + token.screenDelta else @@ -85,6 +89,46 @@ class TokenizedLine getMaxBufferColumn: -> @startBufferColumn + @bufferDelta + # Given a boundary column, finds the point where this line would wrap. + # + # maxColumn - The {Number} where you want soft wrapping to occur + # + # Returns a {Number} representing the `line` position where the wrap would take place. + # Returns `null` if a wrap wouldn't occur. + findWrapColumn: (maxColumn) -> + return unless @text.length > maxColumn + + if /\s/.test(@text[maxColumn]) + # search forward for the start of a word past the boundary + for column in [maxColumn..@text.length] + return column if /\S/.test(@text[column]) + + return @text.length + else + # search backward for the start of the word on the boundary + for column in [maxColumn..0] when @isColumnOutsideSoftWrapIndentation(column) + return column + 1 if /\s/.test(@text[column]) + + return maxColumn + + # Calculates how many trailing spaces in this line's indentation cannot fit in a single tab. + # + # Returns a {Number} representing the odd indentation spaces in this line. + getOddIndentationSpaces: -> + oddIndentLevel = @indentLevel - Math.floor(@indentLevel) + Math.round(@tabLength * oddIndentLevel) + + buildSoftWrapIndentationTokens: (token) -> + indentTokens = [0...Math.floor(@indentLevel)].map => + token.buildSoftWrapIndentationToken(@tabLength) + + if @getOddIndentationSpaces() + indentTokens.concat( + token.buildSoftWrapIndentationToken @getOddIndentationSpaces() + ) + else + indentTokens + softWrapAt: (column) -> return [new TokenizedLine([], '', [0, 0], [0, 0]), this] if column == 0 @@ -98,25 +142,50 @@ class TokenizedLine leftTextLength += nextToken.value.length leftTokens.push nextToken + indentationTokens = @buildSoftWrapIndentationTokens(leftTokens[0]) + leftFragment = new TokenizedLine( tokens: leftTokens startBufferColumn: @startBufferColumn ruleStack: @ruleStack invisibles: @invisibles - lineEnding: null + lineEnding: null, + indentLevel: @indentLevel, + tabLength: @tabLength ) rightFragment = new TokenizedLine( - tokens: rightTokens + tokens: indentationTokens.concat(rightTokens) startBufferColumn: @bufferColumnForScreenColumn(column) ruleStack: @ruleStack invisibles: @invisibles - lineEnding: @lineEnding + lineEnding: @lineEnding, + indentLevel: @indentLevel, + tabLength: @tabLength ) [leftFragment, rightFragment] isSoftWrapped: -> @lineEnding is null + isColumnOutsideSoftWrapIndentation: (column) -> + return true if @softWrapIndentationTokens.length == 0 + + column > @softWrapIndentationDelta + + isColumnInsideSoftWrapIndentation: (column) -> + return false if @softWrapIndentationTokens.length == 0 + + column < @softWrapIndentationDelta + + getSoftWrapIndentationTokens: -> + _.select(@tokens, (token) -> token.isSoftWrapIndentation) + + buildSoftWrapIndentationDelta: -> + _.reduce @softWrapIndentationTokens, ((acc, token) -> acc + token.screenDelta), 0 + + hasOnlySoftWrapIndentation: -> + @tokens.length == @softWrapIndentationTokens.length + tokenAtBufferColumn: (bufferColumn) -> @tokens[@tokenIndexAtBufferColumn(bufferColumn)] @@ -173,7 +242,7 @@ class TokenizedLine changedText = true else if invisibles.space - if token.hasLeadingWhitespace() + if token.hasLeadingWhitespace() and not token.isSoftWrapIndentation token.value = token.value.replace LeadingWhitespaceRegex, (leadingWhitespace) -> leadingWhitespace.replace RepeatedSpaceRegex, invisibles.space token.hasInvisibleCharacters = true