├── .coffeelintignore ├── .github ├── no-response.yml └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── coffeelint.json ├── lib ├── main.coffee └── wrap-guide-element.coffee ├── package.json ├── spec ├── async-spec-helpers.js ├── helpers.js ├── wrap-guide-element-spec.coffee └── wrap-guide-spec.js └── styles └── wrap-guide.less /.coffeelintignore: -------------------------------------------------------------------------------- 1 | spec/fixtures 2 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | channel: [stable, beta] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: UziTech/action-setup-atom@v2 18 | with: 19 | version: ${{ matrix.channel }} 20 | - name: Install dependencies 21 | run: apm install 22 | - name: Run tests 23 | run: atom --test spec 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Wrap Guide package 3 | [![macOS Build Status](https://travis-ci.org/atom/wrap-guide.svg?branch=master)](https://travis-ci.org/atom/wrap-guide) 4 | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5qk1io3uar5j8hol/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/wrap-guide/branch/master) 5 | [![Dependency Status](https://david-dm.org/atom/wrap-guide.svg)](https://david-dm.org/atom/wrap-guide) 6 | 7 | The `wrap-guide` package places a vertical line in each editor at a certain column to guide your formatting, so lines do not exceed a certain width. 8 | 9 | By default, the wrap-guide is placed at the value of `editor.preferredLineLength` config setting. The 80th column is used as the fallback if the config value is unset. 10 | 11 | ![](https://f.cloud.github.com/assets/671378/2241976/dbf6a8f6-9ced-11e3-8fef-d8a226301530.png) 12 | 13 | ## Configuration 14 | 15 | You can customize where the column is placed for different file types by opening the Settings View and configuring the "Preferred Line Length" value. If you do not want the guide to show for a particular language, that can be set using scoped configuration. For example, to turn off the guide for GitHub-Flavored Markdown, you can add the following to your `config.cson`: 16 | 17 | ```coffeescript 18 | '.source.gfm': 19 | 'wrap-guide': 20 | 'enabled': false 21 | ``` 22 | 23 | It is possible to configure the color and/or width of the line by adding the following CSS/LESS to your `styles.less`: 24 | 25 | ```css 26 | atom-text-editor .wrap-guide { 27 | width: 10px; 28 | background-color: red; 29 | } 30 | ``` 31 | 32 | Multiple guide lines are also supported. For example, add the following to your `config.cson` to create four columns at the indicated positions: 33 | 34 | ```coffeescript 35 | 'wrap-guide': 36 | 'columns': [72, 80, 100, 120] 37 | ``` 38 | 39 | > Note: When using multiple guide lines, the right-most guide line functions as your `editor.preferredLineLength` setting. 40 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | WrapGuideElement = require './wrap-guide-element' 3 | 4 | module.exports = 5 | activate: -> 6 | @subscriptions = new CompositeDisposable() 7 | @wrapGuides = new Map() 8 | 9 | @subscriptions.add atom.workspace.observeTextEditors (editor) => 10 | return if @wrapGuides.has(editor) 11 | 12 | editorElement = atom.views.getView(editor) 13 | wrapGuideElement = new WrapGuideElement(editor, editorElement) 14 | 15 | @wrapGuides.set(editor, wrapGuideElement) 16 | @subscriptions.add editor.onDidDestroy => 17 | @wrapGuides.get(editor).destroy() 18 | @wrapGuides.delete(editor) 19 | 20 | deactivate: -> 21 | @subscriptions.dispose() 22 | @wrapGuides.forEach (wrapGuide, editor) -> wrapGuide.destroy() 23 | @wrapGuides.clear() 24 | 25 | uniqueAscending: (list) -> 26 | (list.filter((item, index) -> list.indexOf(item) is index)).sort((a, b) -> a - b) 27 | -------------------------------------------------------------------------------- /lib/wrap-guide-element.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | module.exports = 4 | class WrapGuideElement 5 | constructor: (@editor, @editorElement) -> 6 | @subscriptions = new CompositeDisposable() 7 | @configSubscriptions = new CompositeDisposable() 8 | @element = document.createElement('div') 9 | @element.setAttribute('is', 'wrap-guide') 10 | @element.classList.add('wrap-guide-container') 11 | @attachToLines() 12 | @handleEvents() 13 | @updateGuide() 14 | 15 | @element.updateGuide = @updateGuide.bind(this) 16 | @element.getDefaultColumn = @getDefaultColumn.bind(this) 17 | 18 | attachToLines: -> 19 | scrollView = @editorElement.querySelector('.scroll-view') 20 | scrollView?.appendChild(@element) 21 | 22 | handleEvents: -> 23 | updateGuideCallback = => @updateGuide() 24 | 25 | @handleConfigEvents() 26 | 27 | @subscriptions.add atom.config.onDidChange 'editor.fontSize', => 28 | # Wait for editor to finish updating before updating wrap guide 29 | # TODO: Use async/await once this file is converted to JS 30 | @editorElement.getComponent().getNextUpdatePromise().then -> updateGuideCallback() 31 | 32 | @subscriptions.add @editorElement.onDidChangeScrollLeft(updateGuideCallback) 33 | @subscriptions.add @editor.onDidChangePath(updateGuideCallback) 34 | @subscriptions.add @editor.onDidChangeGrammar => 35 | @configSubscriptions.dispose() 36 | @handleConfigEvents() 37 | updateGuideCallback() 38 | 39 | @subscriptions.add @editor.onDidDestroy => 40 | @subscriptions.dispose() 41 | @configSubscriptions.dispose() 42 | 43 | @subscriptions.add @editorElement.onDidAttach => 44 | @attachToLines() 45 | updateGuideCallback() 46 | 47 | handleConfigEvents: -> 48 | {uniqueAscending} = require './main' 49 | 50 | updatePreferredLineLengthCallback = (args) => 51 | # ensure that the right-most wrap guide is the preferredLineLength 52 | columns = atom.config.get('wrap-guide.columns', scope: @editor.getRootScopeDescriptor()) 53 | if columns.length > 0 54 | columns[columns.length - 1] = args.newValue 55 | columns = uniqueAscending(i for i in columns when i <= args.newValue) 56 | atom.config.set 'wrap-guide.columns', columns, 57 | scopeSelector: ".#{@editor.getGrammar().scopeName}" 58 | @updateGuide() 59 | @configSubscriptions.add atom.config.onDidChange( 60 | 'editor.preferredLineLength', 61 | scope: @editor.getRootScopeDescriptor(), 62 | updatePreferredLineLengthCallback 63 | ) 64 | 65 | updateGuideCallback = => @updateGuide() 66 | @configSubscriptions.add atom.config.onDidChange( 67 | 'wrap-guide.enabled', 68 | scope: @editor.getRootScopeDescriptor(), 69 | updateGuideCallback 70 | ) 71 | 72 | updateGuidesCallback = (args) => 73 | # ensure that multiple guides stay sorted in ascending order 74 | columns = uniqueAscending(args.newValue) 75 | if columns?.length 76 | atom.config.set('wrap-guide.columns', columns) 77 | atom.config.set 'editor.preferredLineLength', columns[columns.length - 1], 78 | scopeSelector: ".#{@editor.getGrammar().scopeName}" 79 | @updateGuide() 80 | @configSubscriptions.add atom.config.onDidChange( 81 | 'wrap-guide.columns', 82 | scope: @editor.getRootScopeDescriptor(), 83 | updateGuidesCallback 84 | ) 85 | 86 | getDefaultColumn: -> 87 | atom.config.get('editor.preferredLineLength', scope: @editor.getRootScopeDescriptor()) 88 | 89 | getGuidesColumns: (path, scopeName) -> 90 | columns = atom.config.get('wrap-guide.columns', scope: @editor.getRootScopeDescriptor()) ? [] 91 | return columns if columns.length > 0 92 | return [@getDefaultColumn()] 93 | 94 | isEnabled: -> 95 | atom.config.get('wrap-guide.enabled', scope: @editor.getRootScopeDescriptor()) ? true 96 | 97 | hide: -> 98 | @element.style.display = 'none' 99 | 100 | show: -> 101 | @element.style.display = 'block' 102 | 103 | updateGuide: -> 104 | if @isEnabled() 105 | @updateGuides() 106 | else 107 | @hide() 108 | 109 | updateGuides: -> 110 | @removeGuides() 111 | @appendGuides() 112 | if @element.children.length 113 | @show() 114 | else 115 | @hide() 116 | 117 | destroy: -> 118 | @element.remove() 119 | @subscriptions.dispose() 120 | @configSubscriptions.dispose() 121 | 122 | removeGuides: -> 123 | while @element.firstChild 124 | @element.removeChild(@element.firstChild) 125 | 126 | appendGuides: -> 127 | columns = @getGuidesColumns(@editor.getPath(), @editor.getGrammar().scopeName) 128 | for column in columns 129 | @appendGuide(column) unless column < 0 130 | 131 | appendGuide: (column) -> 132 | columnWidth = @editorElement.getDefaultCharacterWidth() * column 133 | columnWidth -= @editorElement.getScrollLeft() 134 | guide = document.createElement('div') 135 | guide.classList.add('wrap-guide') 136 | guide.style.left = "#{Math.round(columnWidth)}px" 137 | @element.appendChild(guide) 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wrap-guide", 3 | "version": "0.41.0", 4 | "main": "./lib/main", 5 | "description": "Displays a vertical line at the 80th character in the editor.\nThis packages uses the config value of `editor.preferredLineLength` when set.", 6 | "license": "MIT", 7 | "repository": "https://github.com/atom/wrap-guide", 8 | "engines": { 9 | "atom": "*" 10 | }, 11 | "configSchema": { 12 | "columns": { 13 | "default": [], 14 | "type": "array", 15 | "items": { 16 | "type": "integer" 17 | }, 18 | "description": "Display guides at each of the listed character widths. Leave blank for one guide at your `editor.preferredLineLength`." 19 | }, 20 | "enabled": { 21 | "default": true, 22 | "type": "boolean" 23 | } 24 | }, 25 | "devDependencies": { 26 | "coffeelint": "^1.9.7" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/async-spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | export function beforeEach (fn) { 4 | global.beforeEach(function () { 5 | const result = fn() 6 | if (result instanceof Promise) { 7 | waitsForPromise(() => result) 8 | } 9 | }) 10 | } 11 | 12 | export function afterEach (fn) { 13 | global.afterEach(function () { 14 | const result = fn() 15 | if (result instanceof Promise) { 16 | waitsForPromise(() => result) 17 | } 18 | }) 19 | } 20 | 21 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { 22 | module.exports[name] = function (description, fn) { 23 | if (fn === undefined) { 24 | global[name](description) 25 | return 26 | } 27 | 28 | global[name](description, function () { 29 | const result = fn() 30 | if (result instanceof Promise) { 31 | waitsForPromise(() => result) 32 | } 33 | }) 34 | } 35 | }) 36 | 37 | export async function conditionPromise (condition, description = 'anonymous condition') { 38 | const startTime = Date.now() 39 | 40 | while (true) { 41 | await timeoutPromise(100) 42 | 43 | if (await condition()) { 44 | return 45 | } 46 | 47 | if (Date.now() - startTime > 5000) { 48 | throw new Error('Timed out waiting on ' + description) 49 | } 50 | } 51 | } 52 | 53 | export function timeoutPromise (timeout) { 54 | return new Promise(function (resolve) { 55 | global.setTimeout(resolve, timeout) 56 | }) 57 | } 58 | 59 | function waitsForPromise (fn) { 60 | const promise = fn() 61 | global.waitsFor('spec promise to resolve', function (done) { 62 | promise.then(done, function (error) { 63 | jasmine.getEnv().currentSpec.fail(error) 64 | done() 65 | }) 66 | }) 67 | } 68 | 69 | export function emitterEventPromise (emitter, event, timeout = 15000) { 70 | return new Promise((resolve, reject) => { 71 | const timeoutHandle = setTimeout(() => { 72 | reject(new Error(`Timed out waiting for '${event}' event`)) 73 | }, timeout) 74 | emitter.once(event, () => { 75 | clearTimeout(timeoutHandle) 76 | resolve() 77 | }) 78 | }) 79 | } 80 | 81 | export function promisify (original) { 82 | return function (...args) { 83 | return new Promise((resolve, reject) => { 84 | args.push((err, ...results) => { 85 | if (err) { 86 | reject(err) 87 | } else { 88 | resolve(...results) 89 | } 90 | }) 91 | 92 | return original(...args) 93 | }) 94 | } 95 | } 96 | 97 | export function promisifySome (obj, fnNames) { 98 | const result = {} 99 | for (const fnName of fnNames) { 100 | result[fnName] = promisify(obj[fnName]) 101 | } 102 | return result 103 | } 104 | -------------------------------------------------------------------------------- /spec/helpers.js: -------------------------------------------------------------------------------- 1 | const helpers = { 2 | getWrapGuides () { 3 | wrapGuides = [] 4 | for (const editor of atom.workspace.getTextEditors()) { 5 | const guide = editor.getElement().querySelector('.wrap-guide') 6 | if (guide) wrapGuides.push(guide) 7 | } 8 | return wrapGuides 9 | }, 10 | 11 | getLeftPosition (element) { 12 | return parseInt(element.style.left) 13 | }, 14 | 15 | getLeftPositions (elements) { 16 | return Array.prototype.map.call(elements, element => helpers.getLeftPosition(element)) 17 | } 18 | } 19 | 20 | module.exports = helpers 21 | -------------------------------------------------------------------------------- /spec/wrap-guide-element-spec.coffee: -------------------------------------------------------------------------------- 1 | {getLeftPosition, getLeftPositions} = require './helpers' 2 | {uniqueAscending} = require '../lib/main' 3 | 4 | describe "WrapGuideElement", -> 5 | [editor, editorElement, wrapGuide, workspaceElement] = [] 6 | 7 | beforeEach -> 8 | workspaceElement = atom.views.getView(atom.workspace) 9 | workspaceElement.style.height = "200px" 10 | workspaceElement.style.width = "1500px" 11 | 12 | jasmine.attachToDOM(workspaceElement) 13 | 14 | waitsForPromise -> 15 | atom.packages.activatePackage('wrap-guide') 16 | 17 | waitsForPromise -> 18 | atom.packages.activatePackage('language-javascript') 19 | 20 | waitsForPromise -> 21 | atom.packages.activatePackage('language-coffee-script') 22 | 23 | waitsForPromise -> 24 | atom.workspace.open('sample.js') 25 | 26 | runs -> 27 | editor = atom.workspace.getActiveTextEditor() 28 | editorElement = editor.getElement() 29 | wrapGuide = editorElement.querySelector(".wrap-guide-container") 30 | 31 | describe ".activate", -> 32 | getWrapGuides = -> 33 | wrapGuides = [] 34 | atom.workspace.getTextEditors().forEach (editor) -> 35 | guides = editor.getElement().querySelectorAll(".wrap-guide") 36 | wrapGuides.push(guides) if guides 37 | wrapGuides 38 | 39 | it "appends a wrap guide to all existing and new editors", -> 40 | expect(atom.workspace.getTextEditors().length).toBe 1 41 | 42 | expect(getWrapGuides().length).toBe 1 43 | expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0) 44 | 45 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 46 | expect(atom.workspace.getTextEditors().length).toBe 2 47 | expect(getWrapGuides().length).toBe 2 48 | expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0) 49 | expect(getLeftPosition(getWrapGuides()[1][0])).toBeGreaterThan(0) 50 | 51 | it "positions the guide at the configured column", -> 52 | width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn() 53 | expect(width).toBeGreaterThan(0) 54 | expect(Math.abs(getLeftPosition(wrapGuide.firstChild) - width)).toBeLessThan 1 55 | expect(wrapGuide).toBeVisible() 56 | 57 | it "appends multiple wrap guides to all existing and new editors", -> 58 | columns = [10, 20, 30] 59 | atom.config.set("wrap-guide.columns", columns) 60 | 61 | waitsForPromise -> 62 | editorElement.getComponent().getNextUpdatePromise() 63 | 64 | runs -> 65 | expect(atom.workspace.getTextEditors().length).toBe 1 66 | expect(getWrapGuides().length).toBe 1 67 | positions = getLeftPositions(getWrapGuides()[0]) 68 | expect(positions.length).toBe(columns.length) 69 | expect(positions[0]).toBeGreaterThan(0) 70 | expect(positions[1]).toBeGreaterThan(positions[0]) 71 | expect(positions[2]).toBeGreaterThan(positions[1]) 72 | 73 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 74 | expect(atom.workspace.getTextEditors().length).toBe 2 75 | expect(getWrapGuides().length).toBe 2 76 | pane1_positions = getLeftPositions(getWrapGuides()[0]) 77 | expect(pane1_positions.length).toBe(columns.length) 78 | expect(pane1_positions[0]).toBeGreaterThan(0) 79 | expect(pane1_positions[1]).toBeGreaterThan(pane1_positions[0]) 80 | expect(pane1_positions[2]).toBeGreaterThan(pane1_positions[1]) 81 | pane2_positions = getLeftPositions(getWrapGuides()[1]) 82 | expect(pane2_positions.length).toBe(pane1_positions.length) 83 | expect(pane2_positions[0]).toBe(pane1_positions[0]) 84 | expect(pane2_positions[1]).toBe(pane1_positions[1]) 85 | expect(pane2_positions[2]).toBe(pane1_positions[2]) 86 | 87 | it "positions multiple guides at the configured columns", -> 88 | columnCount = 5 89 | columns = (c * 10 for c in [1..columnCount]) 90 | atom.config.set("wrap-guide.columns", columns) 91 | waitsForPromise -> 92 | editorElement.getComponent().getNextUpdatePromise() 93 | 94 | runs -> 95 | positions = getLeftPositions(getWrapGuides()[0]) 96 | expect(positions.length).toBe(columnCount) 97 | expect(wrapGuide.children.length).toBe(columnCount) 98 | 99 | for i in columnCount - 1 100 | width = editor.getDefaultCharWidth() * columns[i] 101 | expect(width).toBeGreaterThan(0) 102 | expect(Math.abs(getLeftPosition(wrapGuide.children[i]) - width)).toBeLessThan 1 103 | expect(wrapGuide).toBeVisible() 104 | 105 | describe "when the font size changes", -> 106 | it "updates the wrap guide position", -> 107 | initial = getLeftPosition(wrapGuide.firstChild) 108 | expect(initial).toBeGreaterThan(0) 109 | fontSize = atom.config.get("editor.fontSize") 110 | atom.config.set("editor.fontSize", fontSize + 10) 111 | 112 | waitsForPromise -> 113 | editorElement.getComponent().getNextUpdatePromise() 114 | 115 | runs -> 116 | expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial) 117 | expect(wrapGuide.firstChild).toBeVisible() 118 | 119 | it "updates the wrap guide position for hidden editors when they become visible", -> 120 | initial = getLeftPosition(wrapGuide.firstChild) 121 | expect(initial).toBeGreaterThan(0) 122 | 123 | waitsForPromise -> 124 | atom.workspace.open() 125 | 126 | runs -> 127 | fontSize = atom.config.get("editor.fontSize") 128 | atom.config.set("editor.fontSize", fontSize + 10) 129 | atom.workspace.getActivePane().activatePreviousItem() 130 | 131 | waitsForPromise -> 132 | editorElement.getComponent().getNextUpdatePromise() 133 | 134 | runs -> 135 | expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial) 136 | expect(wrapGuide.firstChild).toBeVisible() 137 | 138 | describe "when the column config changes", -> 139 | it "updates the wrap guide position", -> 140 | initial = getLeftPosition(wrapGuide.firstChild) 141 | expect(initial).toBeGreaterThan(0) 142 | column = atom.config.get("editor.preferredLineLength") 143 | atom.config.set("editor.preferredLineLength", column + 10) 144 | expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial) 145 | expect(wrapGuide).toBeVisible() 146 | 147 | describe "when the preferredLineLength changes", -> 148 | it "updates the wrap guide positions", -> 149 | initial = [10, 15, 20, 30] 150 | atom.config.set 'wrap-guide.columns', initial, 151 | scopeSelector: ".#{editor.getGrammar().scopeName}" 152 | waitsForPromise -> 153 | editorElement.getComponent().getNextUpdatePromise() 154 | 155 | runs -> 156 | atom.config.set 'editor.preferredLineLength', 15, 157 | scopeSelector: ".#{editor.getGrammar().scopeName}" 158 | waitsForPromise -> 159 | editorElement.getComponent().getNextUpdatePromise() 160 | 161 | runs -> 162 | columns = atom.config.get('wrap-guide.columns', scope: editor.getRootScopeDescriptor()) 163 | expect(columns.length).toBe(2) 164 | expect(columns[0]).toBe(10) 165 | expect(columns[1]).toBe(15) 166 | 167 | describe "when the columns config changes", -> 168 | it "updates the wrap guide positions", -> 169 | initial = getLeftPositions(wrapGuide.children) 170 | expect(initial.length).toBe(1) 171 | expect(initial[0]).toBeGreaterThan(0) 172 | 173 | columns = [10, 20, 30] 174 | atom.config.set("wrap-guide.columns", columns) 175 | waitsForPromise -> 176 | editorElement.getComponent().getNextUpdatePromise() 177 | 178 | runs -> 179 | positions = getLeftPositions(wrapGuide.children) 180 | expect(positions.length).toBe(columns.length) 181 | expect(positions[0]).toBeGreaterThan(0) 182 | expect(positions[1]).toBeGreaterThan(positions[0]) 183 | expect(positions[2]).toBeGreaterThan(positions[1]) 184 | expect(wrapGuide).toBeVisible() 185 | 186 | it "updates the preferredLineLength", -> 187 | initial = atom.config.get('editor.preferredLineLength', scope: editor.getRootScopeDescriptor()) 188 | atom.config.set("wrap-guide.columns", [initial, initial + 10]) 189 | waitsForPromise -> 190 | editorElement.getComponent().getNextUpdatePromise() 191 | 192 | runs -> 193 | length = atom.config.get('editor.preferredLineLength', scope: editor.getRootScopeDescriptor()) 194 | expect(length).toBe(initial + 10) 195 | 196 | it "keeps guide positions unique and in ascending order", -> 197 | initial = getLeftPositions(wrapGuide.children) 198 | expect(initial.length).toBe(1) 199 | expect(initial[0]).toBeGreaterThan(0) 200 | 201 | reverseColumns = [30, 20, 10] 202 | columns = [reverseColumns[reverseColumns.length - 1], reverseColumns..., reverseColumns[0]] 203 | uniqueColumns = uniqueAscending(columns) 204 | expect(uniqueColumns.length).toBe(3) 205 | expect(uniqueColumns[0]).toBeGreaterThan(0) 206 | expect(uniqueColumns[1]).toBeGreaterThan(uniqueColumns[0]) 207 | expect(uniqueColumns[2]).toBeGreaterThan(uniqueColumns[1]) 208 | 209 | atom.config.set("wrap-guide.columns", columns) 210 | waitsForPromise -> 211 | editorElement.getComponent().getNextUpdatePromise() 212 | 213 | runs -> 214 | positions = getLeftPositions(wrapGuide.children) 215 | expect(positions.length).toBe(uniqueColumns.length) 216 | expect(positions[0]).toBeGreaterThan(0) 217 | expect(positions[1]).toBeGreaterThan(positions[0]) 218 | expect(positions[2]).toBeGreaterThan(positions[1]) 219 | expect(wrapGuide).toBeVisible() 220 | 221 | describe "when the editor's scroll left changes", -> 222 | it "updates the wrap guide position to a relative position on screen", -> 223 | editor.setText("a long line which causes the editor to scroll") 224 | editorElement.style.width = "100px" 225 | 226 | waitsFor -> editorElement.component.getMaxScrollLeft() > 10 227 | 228 | runs -> 229 | initial = getLeftPosition(wrapGuide.firstChild) 230 | expect(initial).toBeGreaterThan(0) 231 | editorElement.setScrollLeft(10) 232 | expect(getLeftPosition(wrapGuide.firstChild)).toBe(initial - 10) 233 | expect(wrapGuide.firstChild).toBeVisible() 234 | 235 | describe "when the editor's grammar changes", -> 236 | it "updates the wrap guide position", -> 237 | atom.config.set('editor.preferredLineLength', 20, scopeSelector: '.source.js') 238 | initial = getLeftPosition(wrapGuide.firstChild) 239 | expect(initial).toBeGreaterThan(0) 240 | expect(wrapGuide).toBeVisible() 241 | 242 | editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar')) 243 | expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial) 244 | expect(wrapGuide).toBeVisible() 245 | 246 | it 'listens for preferredLineLength updates for the new grammar', -> 247 | editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee')) 248 | initial = getLeftPosition(wrapGuide.firstChild) 249 | atom.config.set('editor.preferredLineLength', 20, scopeSelector: '.source.coffee') 250 | expect(getLeftPosition(wrapGuide.firstChild)).toBeLessThan(initial) 251 | 252 | it 'listens for wrap-guide.enabled updates for the new grammar', -> 253 | editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee')) 254 | expect(wrapGuide).toBeVisible() 255 | atom.config.set('wrap-guide.enabled', false, scopeSelector: '.source.coffee') 256 | expect(wrapGuide).not.toBeVisible() 257 | 258 | describe 'scoped config', -> 259 | it '::getDefaultColumn returns the scope-specific column value', -> 260 | atom.config.set('editor.preferredLineLength', 132, scopeSelector: '.source.js') 261 | 262 | expect(wrapGuide.getDefaultColumn()).toBe 132 263 | 264 | it 'updates the guide when the scope-specific column changes', -> 265 | initial = getLeftPosition(wrapGuide.firstChild) 266 | column = atom.config.get('editor.preferredLineLength', scope: editor.getRootScopeDescriptor()) 267 | atom.config.set('editor.preferredLineLength', column + 10, scope: '.source.js') 268 | expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial) 269 | 270 | it 'updates the guide when wrap-guide.enabled is set to false', -> 271 | expect(wrapGuide).toBeVisible() 272 | 273 | atom.config.set('wrap-guide.enabled', false, scopeSelector: '.source.js') 274 | 275 | expect(wrapGuide).not.toBeVisible() 276 | -------------------------------------------------------------------------------- /spec/wrap-guide-spec.js: -------------------------------------------------------------------------------- 1 | const {getWrapGuides, getLeftPosition} = require('./helpers') 2 | 3 | const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars 4 | 5 | describe('Wrap Guide', () => { 6 | let editor, editorElement, wrapGuide = [] 7 | 8 | beforeEach(async () => { 9 | await atom.packages.activatePackage('wrap-guide') 10 | 11 | editor = await atom.workspace.open('sample.js') 12 | editorElement = editor.getElement() 13 | wrapGuide = editorElement.querySelector('.wrap-guide-container') 14 | 15 | jasmine.attachToDOM(atom.views.getView(atom.workspace)) 16 | }) 17 | 18 | describe('package activation', () => { 19 | it('appends a wrap guide to all existing and new editors', () => { 20 | expect(atom.workspace.getTextEditors().length).toBe(1) 21 | expect(getWrapGuides().length).toBe(1) 22 | expect(getLeftPosition(getWrapGuides()[0])).toBeGreaterThan(0) 23 | 24 | atom.workspace.getActivePane().splitRight({copyActiveItem: true}) 25 | expect(atom.workspace.getTextEditors().length).toBe(2) 26 | expect(getWrapGuides().length).toBe(2) 27 | expect(getLeftPosition(getWrapGuides()[0])).toBeGreaterThan(0) 28 | expect(getLeftPosition(getWrapGuides()[1])).toBeGreaterThan(0) 29 | }) 30 | 31 | it('positions the guide at the configured column', () => { 32 | width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn() 33 | expect(width).toBeGreaterThan(0) 34 | expect(Math.abs(getLeftPosition(wrapGuide.firstChild) - width)).toBeLessThan(1) 35 | expect(wrapGuide.firstChild).toBeVisible() 36 | }) 37 | }) 38 | 39 | describe('package deactivation', () => { 40 | beforeEach(async () => { 41 | await atom.packages.deactivatePackage('wrap-guide') 42 | }) 43 | 44 | it('disposes of all wrap guides', () => { 45 | expect(getWrapGuides().length).toBe(0) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /styles/wrap-guide.less: -------------------------------------------------------------------------------- 1 | @import "syntax-variables"; 2 | 3 | atom-text-editor { 4 | .wrap-guide { 5 | height: 100%; 6 | width: 1px; 7 | z-index: 3; 8 | position: absolute; 9 | top: 0; 10 | background-color: @syntax-wrap-guide-color; 11 | -webkit-transform: translateZ(0); 12 | pointer-events: none; 13 | } 14 | } 15 | --------------------------------------------------------------------------------