diff --git a/package-lock.json b/package-lock.json index eed8892ea..ba19175ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2544,8 +2544,7 @@ "integrity": "sha1-hJgDKzttHMge68X3lpDY/in6v08=" }, "go-to-line": { - "version": "https://www.atom.io/api/packages/go-to-line/versions/0.33.0/tarball", - "integrity": "sha512-YD5zEkGQRTl6jrgAIOQ0Zr0rB/f/yPifxhz4od2kN+JfGVeb76xD9FG5OkR9dr0vEtPfJeDbG2WWTJ1JGMShqQ==" + "version": "file:packages/go-to-line" }, "graceful-fs": { "version": "4.1.11", diff --git a/package.json b/package.json index c1eb18674..d4342f695 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "git-utils": "5.2.1", "github": "https://www.atom.io/api/packages/github/versions/0.20.1/tarball", "glob": "^7.1.1", - "go-to-line": "https://www.atom.io/api/packages/go-to-line/versions/0.33.0/tarball", + "go-to-line": "file:packages/go-to-line", "grammar-selector": "https://www.atom.io/api/packages/grammar-selector/versions/0.50.1/tarball", "grim": "1.5.0", "image-view": "https://www.atom.io/api/packages/image-view/versions/0.63.1/tarball", @@ -204,7 +204,7 @@ "fuzzy-finder": "1.8.2", "github": "0.20.1", "git-diff": "file:./packages/git-diff", - "go-to-line": "0.33.0", + "go-to-line": "file:./packages/go-to-line", "grammar-selector": "0.50.1", "image-view": "0.63.1", "incompatible-packages": "file:./packages/incompatible-packages", diff --git a/packages/README.md b/packages/README.md index 3cc90bdec..04d8946fc 100644 --- a/packages/README.md +++ b/packages/README.md @@ -35,7 +35,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **fuzzy-finder** | [`atom/fuzzy-finder`][fuzzy-finder] | | | **github** | [`atom/github`][github] | | | **git-diff** | [`./git-diff`](./git-diff) | [#17843](https://github.com/atom/atom/issues/17843) | -| **go-to-line** | [`atom/go-to-line`][go-to-line] | [#17844](https://github.com/atom/atom/issues/17844) | +| **go-to-line** | [`./go-to-line`](./go-to-line) | [#17844](https://github.com/atom/atom/issues/17844) | | **grammar-selector** | [`atom/grammar-selector`][grammar-selector] | [#17845](https://github.com/atom/atom/issues/17845) | | **image-view** | [`atom/image-view`][image-view] | | | **incompatible-packages** | [`./incompatible-packages`](./incompatible-packages) | [#17846](https://github.com/atom/atom/issues/17846) | @@ -115,7 +115,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate [find-and-replace]: https://github.com/atom/find-and-replace [fuzzy-finder]: https://github.com/atom/fuzzy-finder [github]: https://github.com/atom/github -[go-to-line]: https://github.com/atom/go-to-line [grammar-selector]: https://github.com/atom/grammar-selector [image-view]: https://github.com/atom/image-view [keybinding-resolver]: https://github.com/atom/keybinding-resolver diff --git a/packages/go-to-line/.gitignore b/packages/go-to-line/.gitignore new file mode 100644 index 000000000..b271ae615 --- /dev/null +++ b/packages/go-to-line/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +Thumbs.db +node_modules +npm-debug.log diff --git a/packages/go-to-line/LICENSE.md b/packages/go-to-line/LICENSE.md new file mode 100644 index 000000000..4d231b456 --- /dev/null +++ b/packages/go-to-line/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/go-to-line/README.md b/packages/go-to-line/README.md new file mode 100644 index 000000000..d29ccff74 --- /dev/null +++ b/packages/go-to-line/README.md @@ -0,0 +1,5 @@ +# Go To Line package + +Move the cursor to a specific line in the editor using ctrl-g. + +![](https://f.cloud.github.com/assets/671378/2241602/fdd88c4c-9cd8-11e3-9d14-74844ec7da01.png) diff --git a/packages/go-to-line/keymaps/go-to-line.cson b/packages/go-to-line/keymaps/go-to-line.cson new file mode 100644 index 000000000..2d588a2b7 --- /dev/null +++ b/packages/go-to-line/keymaps/go-to-line.cson @@ -0,0 +1,15 @@ +'.platform-darwin, .platform-win32, .platform-linux': + 'ctrl-g': 'go-to-line:toggle' + +'.go-to-line atom-text-editor[mini]': + 'enter': 'core:confirm', + 'escape': 'core:cancel' + +'.platform-darwin .go-to-line atom-text-editor[mini]': + 'cmd-w': 'core:cancel' + +'.platform-win32 .go-to-line atom-text-editor[mini]': + 'ctrl-w': 'core:cancel' + +'.platform-linux .go-to-line atom-text-editor[mini]': + 'ctrl-w': 'core:cancel' diff --git a/packages/go-to-line/lib/go-to-line-view.js b/packages/go-to-line/lib/go-to-line-view.js new file mode 100644 index 000000000..66759f9d9 --- /dev/null +++ b/packages/go-to-line/lib/go-to-line-view.js @@ -0,0 +1,105 @@ +'use babel' + +import { Point, TextEditor } from 'atom' + +class GoToLineView { + constructor () { + this.miniEditor = new TextEditor({ mini: true }) + this.miniEditor.element.addEventListener('blur', this.close.bind(this)) + + this.message = document.createElement('div') + this.message.classList.add('message') + + this.element = document.createElement('div') + this.element.classList.add('go-to-line') + this.element.appendChild(this.miniEditor.element) + this.element.appendChild(this.message) + + this.panel = atom.workspace.addModalPanel({ + item: this, + visible: false + }) + atom.commands.add('atom-text-editor', 'go-to-line:toggle', () => { + this.toggle() + return false + }) + atom.commands.add(this.miniEditor.element, 'core:confirm', () => { + this.navigate() + }) + atom.commands.add(this.miniEditor.element, 'core:cancel', () => { + this.close() + }) + this.miniEditor.onWillInsertText((arg) => { + if (arg.text.match(/[^0-9:]/)) { + arg.cancel() + } + }) + this.miniEditor.onDidChange(() => { + this.navigate({keepOpen: true}) + }) + } + + toggle () { + this.panel.isVisible() ? this.close() : this.open() + } + + close () { + if (!this.panel.isVisible()) return + this.miniEditor.setText('') + this.panel.hide() + if (this.miniEditor.element.hasFocus()) { + this.restoreFocus() + } + } + + navigate (options = {}) { + const lineNumber = this.miniEditor.getText() + const editor = atom.workspace.getActiveTextEditor() + if (!options.keepOpen) { + this.close() + } + if (!editor || !lineNumber.length) return + + const currentRow = editor.getCursorBufferPosition().row + const rowLineNumber = lineNumber.split(/:+/)[0] || '' + const row = rowLineNumber.length > 0 ? parseInt(rowLineNumber) - 1 : currentRow + const columnLineNumber = lineNumber.split(/:+/)[1] || '' + const column = columnLineNumber.length > 0 ? parseInt(columnLineNumber) - 1 : -1 + + const position = new Point(row, column) + editor.setCursorBufferPosition(position) + editor.unfoldBufferRow(row) + if (column < 0) { + editor.moveToFirstCharacterOfLine() + } + editor.scrollToBufferPosition(position, { + center: true + }) + } + + storeFocusedElement () { + this.previouslyFocusedElement = document.activeElement + return this.previouslyFocusedElement + } + + restoreFocus () { + if (this.previouslyFocusedElement && this.previouslyFocusedElement.parentElement) { + return this.previouslyFocusedElement.focus() + } + atom.views.getView(atom.workspace).focus() + } + + open () { + if (this.panel.isVisible() || !atom.workspace.getActiveTextEditor()) return + this.storeFocusedElement() + this.panel.show() + this.message.textContent = 'Enter a or : to go there. Examples: "3" for row 3 or "2:7" for row 2 and column 7' + this.miniEditor.element.focus() + } +} + +export default { + activate () { + return new GoToLineView() + } +} diff --git a/packages/go-to-line/menus/go-to-line.cson b/packages/go-to-line/menus/go-to-line.cson new file mode 100644 index 000000000..60a220e45 --- /dev/null +++ b/packages/go-to-line/menus/go-to-line.cson @@ -0,0 +1,7 @@ +'menu': [ + 'label': 'Edit' + 'submenu': [ + 'label': 'Go to Line' + 'command': 'go-to-line:toggle' + ] +] diff --git a/packages/go-to-line/package.json b/packages/go-to-line/package.json new file mode 100644 index 000000000..4f6e87581 --- /dev/null +++ b/packages/go-to-line/package.json @@ -0,0 +1,31 @@ +{ + "name": "go-to-line", + "version": "0.33.0", + "main": "./lib/go-to-line-view", + "description": "Jump to a specific editor line number with `ctrl-g`.", + "license": "MIT", + "scripts": { + "lint": "standard" + }, + "activationCommands": { + "atom-text-editor": [ + "go-to-line:toggle" + ] + }, + "repository": "https://github.com/atom/atom", + "engines": { + "atom": "*" + }, + "devDependencies": { + "standard": "^8.6.0" + }, + "standard": { + "globals": [ + "atom", + "waitsForPromise" + ], + "ignore": [ + "spec/fixtures" + ] + } +} diff --git a/packages/go-to-line/spec/fixtures/sample.js b/packages/go-to-line/spec/fixtures/sample.js new file mode 100644 index 000000000..cb53d4078 --- /dev/null +++ b/packages/go-to-line/spec/fixtures/sample.js @@ -0,0 +1,70 @@ +var quicksort = function () { + var sort = function(items) { + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + return sort(left).concat(pivot).concat(sort(right)); + }; + + return sort(Array.apply(this, arguments)); +}; + +// adapted from: +// https://github.com/nzakas/computer-science-in-javascript/tree/master/algorithms/sorting/merge-sort-recursive +var mergeSort function (items){ + var merge = function (left, right){ + var result = []; + var il = 0; + var ir = 0; + + while (il < left.length && ir < right.length){ + if (left[il] < right[ir]){ + result.push(left[il++]); + } else { + result.push(right[ir++]); + } + } + + return result.concat(left.slice(il)).concat(right.slice(ir)); + }; + + if (items.length < 2) { + return items; + } + + var middle = Math.floor(items.length / 2), + left = items.slice(0, middle), + right = items.slice(middle), + params = merge(mergeSort(left), mergeSort(right)); + + // Add the arguments to replace everything between 0 and last item in the array + params.unshift(0, items.length); + items.splice.apply(items, params); + return items; +}; + +// adapted from: +// https://github.com/nzakas/computer-science-in-javascript/blob/master/algorithms/sorting/bubble-sort/bubble-sort.js +var bubbleSort = function (items){ + var swap = function (items, firstIndex, secondIndex){ + var temp = items[firstIndex]; + items[firstIndex] = items[secondIndex]; + items[secondIndex] = temp; + }; + + var len = items.length, + i, j, stop; + + for (i=0; i < len; i++){ + for (j=0, stop=len-i; j < stop; j++){ + if (items[j] > items[j+1]){ + swap(items, j, j+1); + } + } + } + + return items; +}; diff --git a/packages/go-to-line/spec/go-to-line-spec.js b/packages/go-to-line/spec/go-to-line-spec.js new file mode 100644 index 000000000..8b9f3ba92 --- /dev/null +++ b/packages/go-to-line/spec/go-to-line-spec.js @@ -0,0 +1,165 @@ +'use babel' + +/* eslint-env jasmine */ + +import GoToLineView from '../lib/go-to-line-view' + +describe('GoToLine', () => { + let editor = null + let editorView = null + let goToLine = null + + beforeEach(() => { + waitsForPromise(() => { + return atom.workspace.open('sample.js') + }) + + runs(() => { + const workspaceElement = atom.views.getView(atom.workspace) + workspaceElement.style.height = '200px' + workspaceElement.style.width = '1000px' + jasmine.attachToDOM(workspaceElement) + editor = atom.workspace.getActiveTextEditor() + editorView = atom.views.getView(editor) + goToLine = GoToLineView.activate() + editor.setCursorBufferPosition([1, 0]) + }) + }) + + describe('when go-to-line:toggle is triggered', () => { + it('adds a modal panel', () => { + expect(goToLine.panel.isVisible()).toBeFalsy() + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + }) + }) + + describe('when entering a line number', () => { + it('only allows 0-9 and the colon character to be entered in the mini editor', () => { + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText('a') + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText('path/file.txt:56') + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText(':') + expect(goToLine.miniEditor.getText()).toBe(':') + goToLine.miniEditor.setText('') + goToLine.miniEditor.insertText('4') + expect(goToLine.miniEditor.getText()).toBe('4') + }) + }) + + describe('when typing line numbers (auto-navigation)', () => { + it('automatically scrolls to the desired line', () => { + goToLine.miniEditor.insertText('13') + expect(editor.getCursorBufferPosition()).toEqual([12, 0]) + }) + }) + + describe('when typing line and column numbers (auto-navigation)', () => { + it('automatically scrolls to the desired line and column', () => { + goToLine.miniEditor.insertText('3:8') + expect(editor.getCursorBufferPosition()).toEqual([2, 7]) + }) + }) + + describe('when entering a line number and column number', () => { + it('moves the cursor to the column number of the line specified', () => { + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText('3:14') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(editor.getCursorBufferPosition()).toEqual([2, 13]) + }) + + it('centers the selected line', () => { + goToLine.miniEditor.insertText('45:4') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + const rowsPerPage = editor.getRowsPerPage() + const currentRow = editor.getCursorBufferPosition().row - 1 + expect(editor.getFirstVisibleScreenRow()).toBe(currentRow - Math.ceil(rowsPerPage / 2)) + expect(editor.getLastVisibleScreenRow()).toBe(currentRow + Math.floor(rowsPerPage / 2)) + }) + }) + + describe('when entering a line number greater than the number of rows in the buffer', () => { + it('moves the cursor position to the first character of the last line', () => { + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText('71') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([70, 0]) + }) + }) + + describe('when entering a column number greater than the number in the specified line', () => { + it('moves the cursor position to the last character of the specified line', () => { + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + expect(goToLine.miniEditor.getText()).toBe('') + goToLine.miniEditor.insertText('3:43') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([2, 40]) + }) + }) + + describe('when core:confirm is triggered', () => { + describe('when a line number has been entered', () => { + it('moves the cursor to the first character of the line', () => { + goToLine.miniEditor.insertText('3') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(editor.getCursorBufferPosition()).toEqual([2, 4]) + }) + }) + + describe('when the line number entered is nested within foldes', () => { + it('unfolds all folds containing the given row', () => { + expect(editor.indentationForBufferRow(6)).toEqual(3) + editor.foldAll() + expect(editor.screenRowForBufferRow(6)).toEqual(0) + goToLine.miniEditor.insertText('7') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(editor.getCursorBufferPosition()).toEqual([6, 6]) + }) + }) + }) + + describe('when no line number has been entered', () => { + it('closes the view and does not update the cursor position', () => { + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + + describe('when no line number has been entered, but a column number has been entered', () => { + it('navigates to the column of the current line', () => { + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + goToLine.miniEditor.insertText('4:1') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([3, 0]) + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + goToLine.miniEditor.insertText(':19') + atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([3, 18]) + }) + }) + + describe('when core:cancel is triggered', () => { + it('closes the view and does not update the cursor position', () => { + atom.commands.dispatch(editorView, 'go-to-line:toggle') + expect(goToLine.panel.isVisible()).toBeTruthy() + atom.commands.dispatch(goToLine.miniEditor.element, 'core:cancel') + expect(goToLine.panel.isVisible()).toBeFalsy() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) +})