From 2d8be51e71793e3cf313fcda3acff8bb520006b9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 3 Oct 2012 17:40:59 -0700 Subject: [PATCH 01/83] Add initial editor command super class This can be extended by extensions targetted towards acting on text inside the editor and not contributing any UI --- spec/extensions/editor-command-spec.coffee | 150 +++++++++++++++++++++ src/extensions/editor-command.coffee | 33 +++++ src/extensions/lowercase-command.coffee | 11 ++ src/extensions/uppercase-command.coffee | 11 ++ 4 files changed, 205 insertions(+) create mode 100644 spec/extensions/editor-command-spec.coffee create mode 100644 src/extensions/editor-command.coffee create mode 100644 src/extensions/lowercase-command.coffee create mode 100644 src/extensions/uppercase-command.coffee diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee new file mode 100644 index 000000000..9b1b9cf03 --- /dev/null +++ b/spec/extensions/editor-command-spec.coffee @@ -0,0 +1,150 @@ +EditorCommand = require 'editor-command' +LowerCaseCommand = require 'lowercase-command' +UpperCaseCommand = require 'uppercase-command' +RootView = require 'root-view' +fs = require 'fs' + +describe "EditorCommand", -> + [rootView, editor, path] = [] + + beforeEach -> + rootView = new RootView + rootView.open(require.resolve 'fixtures/sample.js') + + rootView.focus() + editor = rootView.getActiveEditor() + + afterEach -> + rootView.remove() + + describe "@alterSelection()", -> + it "returns true when transformed text is non-empty", -> + transformed = false + altered = false + class CustomCommand extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'custom' + + @execute: (editor, event) -> + altered = @alterSelection editor, (text) -> + transformed = true + 'new' + + CustomCommand.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'custom' + expect(transformed).toBe true + expect(altered).toBe true + + it "returns false when transformed text is null", -> + transformed = false + altered = false + class CustomCommand extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'custom' + + @execute: (editor, event) -> + altered = @alterSelection editor, (text) -> + transformed = true + null + + CustomCommand.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'custom' + expect(transformed).toBe true + expect(altered).toBe false + + it "returns false when transformed text is undefined", -> + transformed = false + altered = false + class CustomCommand extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'custom' + + @execute: (editor, event) -> + altered = @alterSelection editor, (text) -> + transformed = true + undefined + + CustomCommand.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'custom' + expect(transformed).toBe true + expect(altered).toBe false + + describe "custom sub-class", -> + it "removes vowels from selected text", -> + class VowelRemover extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'devowel' + + @execute: (editor, event) -> + @alterSelection editor, (text) -> + text.replace(/[aeiouy]/gi, '') + + VowelRemover.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'devowel' + expect(editor.lineForBufferRow(0)).toBe 'vr qcksrt = fnctn () {' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'vr qcksrt = fnctn () {' + + it "doesn't transform empty selections", -> + callbackCount = 0 + class CustomCommand extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'custom' + + @execute: (editor, event) -> + @alterSelection editor, (text) -> + callbackCount++ + text + + CustomCommand.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'custom' + expect(callbackCount).toBe 1 + editor.clearSelections() + editor.trigger 'custom' + expect(callbackCount).toBe 1 + + it "registers all keymaps", -> + callbackCount = 0 + class CustomCommand extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'custom1' + 'meta-B': 'custom2' + + @execute: (editor, event) -> + @alterSelection editor, (text) -> + callbackCount++ + text + + CustomCommand.activate(rootView) + editor.moveCursorToTop() + editor.selectToEndOfLine() + editor.trigger 'custom1' + expect(callbackCount).toBe 1 + editor.trigger 'custom2' + expect(callbackCount).toBe 2 + + describe "LowerCaseCommand", -> + it "replaces the selected text with all lower case characters", -> + LowerCaseCommand.activate(rootView) + editor.setSelectedBufferRange([[11,14], [11,19]]) + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'Array' + editor.trigger 'lowercase' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'array' + + + describe "UpperCaseCommand", -> + it "replaces the selected text with all upper case characters", -> + UpperCaseCommand.activate(rootView) + editor.setSelectedBufferRange([[0,0], [0,3]]) + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'var' + editor.trigger 'uppercase' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'VAR' diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee new file mode 100644 index 000000000..100c9ded7 --- /dev/null +++ b/src/extensions/editor-command.coffee @@ -0,0 +1,33 @@ +module.exports = +class EditorCommand + + @activate: (rootView) -> + keymaps = @getKeymaps() + return unless keymaps + + window.keymap.bindKeys '.editor', keymaps + + for editor in rootView.getEditors() + @subscribeToEditor(rootView, editor) + + rootView.on 'editor-open', (e, editor) => + @subscribeToEditor(rootView, editor) + + @subscribeToEditor: (rootView, editor) -> + keymaps = @getKeymaps(rootView, editor) + return unless keymaps + + for key, event of keymaps + editor.on event, => @execute(editor, event) + + @alterSelection: (editor, transform) -> + selection = editor.getSelection() + return false if selection.isEmpty() + + range = selection.getBufferRange() + reverse = selection.isReversed() + text = transform(editor.getTextInRange(range)) + return false if text is null or text is undefined + editor.insertText(text) + selection.setBufferRange(range, {reverse}) + true diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee new file mode 100644 index 000000000..57ac1242d --- /dev/null +++ b/src/extensions/lowercase-command.coffee @@ -0,0 +1,11 @@ +EditorCommand = require 'editor-command' + +module.exports = +class LowerCaseCommand extends EditorCommand + + @getKeymaps: (editor) -> + 'meta-Y': 'lowercase' + + @execute: (editor, event) -> + @alterSelection editor, (text) -> + text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee new file mode 100644 index 000000000..5a63f87e8 --- /dev/null +++ b/src/extensions/uppercase-command.coffee @@ -0,0 +1,11 @@ +EditorCommand = require 'editor-command' + +module.exports = +class UpperCaseCommand extends EditorCommand + + @getKeymaps: (editor) -> + 'meta-X': 'uppercase' + + @execute: (editor, event) -> + @alterSelection editor, (text) -> + text.toUpperCase() From d93a1422634640b88332c71a2f400cbd41ac818a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 3 Oct 2012 21:07:50 -0700 Subject: [PATCH 02/83] Rename alterSelection to editSelectedText --- spec/extensions/editor-command-spec.coffee | 26 +++++++++++----------- src/extensions/editor-command.coffee | 2 +- src/extensions/lowercase-command.coffee | 2 +- src/extensions/uppercase-command.coffee | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee index 9b1b9cf03..e6d8e4234 100644 --- a/spec/extensions/editor-command-spec.coffee +++ b/spec/extensions/editor-command-spec.coffee @@ -17,16 +17,16 @@ describe "EditorCommand", -> afterEach -> rootView.remove() - describe "@alterSelection()", -> + describe "@editSelectedText()", -> it "returns true when transformed text is non-empty", -> transformed = false - altered = false + edited = false class CustomCommand extends EditorCommand @getKeymaps: (editor) -> 'meta-V': 'custom' @execute: (editor, event) -> - altered = @alterSelection editor, (text) -> + edited = @editSelectedText editor, (text) -> transformed = true 'new' @@ -35,17 +35,17 @@ describe "EditorCommand", -> editor.selectToEndOfLine() editor.trigger 'custom' expect(transformed).toBe true - expect(altered).toBe true + expect(edited).toBe true it "returns false when transformed text is null", -> transformed = false - altered = false + edited = false class CustomCommand extends EditorCommand @getKeymaps: (editor) -> 'meta-V': 'custom' @execute: (editor, event) -> - altered = @alterSelection editor, (text) -> + edited = @editSelectedText editor, (text) -> transformed = true null @@ -54,17 +54,17 @@ describe "EditorCommand", -> editor.selectToEndOfLine() editor.trigger 'custom' expect(transformed).toBe true - expect(altered).toBe false + expect(edited).toBe false it "returns false when transformed text is undefined", -> transformed = false - altered = false + edited = false class CustomCommand extends EditorCommand @getKeymaps: (editor) -> 'meta-V': 'custom' @execute: (editor, event) -> - altered = @alterSelection editor, (text) -> + edited = @editSelectedText editor, (text) -> transformed = true undefined @@ -73,7 +73,7 @@ describe "EditorCommand", -> editor.selectToEndOfLine() editor.trigger 'custom' expect(transformed).toBe true - expect(altered).toBe false + expect(edited).toBe false describe "custom sub-class", -> it "removes vowels from selected text", -> @@ -82,7 +82,7 @@ describe "EditorCommand", -> 'meta-V': 'devowel' @execute: (editor, event) -> - @alterSelection editor, (text) -> + @editSelectedText editor, (text) -> text.replace(/[aeiouy]/gi, '') VowelRemover.activate(rootView) @@ -99,7 +99,7 @@ describe "EditorCommand", -> 'meta-V': 'custom' @execute: (editor, event) -> - @alterSelection editor, (text) -> + @editSelectedText editor, (text) -> callbackCount++ text @@ -120,7 +120,7 @@ describe "EditorCommand", -> 'meta-B': 'custom2' @execute: (editor, event) -> - @alterSelection editor, (text) -> + @editSelectedText editor, (text) -> callbackCount++ text diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee index 100c9ded7..f3310b2ff 100644 --- a/src/extensions/editor-command.coffee +++ b/src/extensions/editor-command.coffee @@ -20,7 +20,7 @@ class EditorCommand for key, event of keymaps editor.on event, => @execute(editor, event) - @alterSelection: (editor, transform) -> + @editSelectedText: (editor, transform) -> selection = editor.getSelection() return false if selection.isEmpty() diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index 57ac1242d..51bc9654a 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -7,5 +7,5 @@ class LowerCaseCommand extends EditorCommand 'meta-Y': 'lowercase' @execute: (editor, event) -> - @alterSelection editor, (text) -> + @editSelectedText editor, (text) -> text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index 5a63f87e8..2bc7e8e18 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -7,5 +7,5 @@ class UpperCaseCommand extends EditorCommand 'meta-X': 'uppercase' @execute: (editor, event) -> - @alterSelection editor, (text) -> + @editSelectedText editor, (text) -> text.toUpperCase() From 863f9f36fbe0437b8bc1a1b3cdfac485f6dd4e9a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 3 Oct 2012 21:52:50 -0700 Subject: [PATCH 03/83] Rename editSelectedText to replaceSelectedText --- spec/extensions/editor-command-spec.coffee | 14 +++++++------- src/extensions/editor-command.coffee | 4 ++-- src/extensions/lowercase-command.coffee | 2 +- src/extensions/uppercase-command.coffee | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee index e6d8e4234..27912bd71 100644 --- a/spec/extensions/editor-command-spec.coffee +++ b/spec/extensions/editor-command-spec.coffee @@ -17,7 +17,7 @@ describe "EditorCommand", -> afterEach -> rootView.remove() - describe "@editSelectedText()", -> + describe "@replaceSelectedText()", -> it "returns true when transformed text is non-empty", -> transformed = false edited = false @@ -26,7 +26,7 @@ describe "EditorCommand", -> 'meta-V': 'custom' @execute: (editor, event) -> - edited = @editSelectedText editor, (text) -> + edited = @replaceSelectedText editor, (text) -> transformed = true 'new' @@ -45,7 +45,7 @@ describe "EditorCommand", -> 'meta-V': 'custom' @execute: (editor, event) -> - edited = @editSelectedText editor, (text) -> + edited = @replaceSelectedText editor, (text) -> transformed = true null @@ -64,7 +64,7 @@ describe "EditorCommand", -> 'meta-V': 'custom' @execute: (editor, event) -> - edited = @editSelectedText editor, (text) -> + edited = @replaceSelectedText editor, (text) -> transformed = true undefined @@ -82,7 +82,7 @@ describe "EditorCommand", -> 'meta-V': 'devowel' @execute: (editor, event) -> - @editSelectedText editor, (text) -> + @replaceSelectedText editor, (text) -> text.replace(/[aeiouy]/gi, '') VowelRemover.activate(rootView) @@ -99,7 +99,7 @@ describe "EditorCommand", -> 'meta-V': 'custom' @execute: (editor, event) -> - @editSelectedText editor, (text) -> + @replaceSelectedText editor, (text) -> callbackCount++ text @@ -120,7 +120,7 @@ describe "EditorCommand", -> 'meta-B': 'custom2' @execute: (editor, event) -> - @editSelectedText editor, (text) -> + @replaceSelectedText editor, (text) -> callbackCount++ text diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee index f3310b2ff..22d11adda 100644 --- a/src/extensions/editor-command.coffee +++ b/src/extensions/editor-command.coffee @@ -20,13 +20,13 @@ class EditorCommand for key, event of keymaps editor.on event, => @execute(editor, event) - @editSelectedText: (editor, transform) -> + @replaceSelectedText: (editor, replace) -> selection = editor.getSelection() return false if selection.isEmpty() range = selection.getBufferRange() reverse = selection.isReversed() - text = transform(editor.getTextInRange(range)) + text = replace(editor.getTextInRange(range)) return false if text is null or text is undefined editor.insertText(text) selection.setBufferRange(range, {reverse}) diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index 51bc9654a..1dd6d7a33 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -7,5 +7,5 @@ class LowerCaseCommand extends EditorCommand 'meta-Y': 'lowercase' @execute: (editor, event) -> - @editSelectedText editor, (text) -> + @replaceSelectedText editor, (text) -> text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index 2bc7e8e18..a79b1969b 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -7,5 +7,5 @@ class UpperCaseCommand extends EditorCommand 'meta-X': 'uppercase' @execute: (editor, event) -> - @editSelectedText editor, (text) -> + @replaceSelectedText editor, (text) -> text.toUpperCase() From ceb496e202908c035d3ea56302c9227fc840b78e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Oct 2012 11:07:46 -0700 Subject: [PATCH 04/83] Use closure wrapper with current event name --- spec/extensions/editor-command-spec.coffee | 4 ++++ src/extensions/editor-command.coffee | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee index 27912bd71..eda5d9c7e 100644 --- a/spec/extensions/editor-command-spec.coffee +++ b/spec/extensions/editor-command-spec.coffee @@ -114,12 +114,14 @@ describe "EditorCommand", -> it "registers all keymaps", -> callbackCount = 0 + eventName = null class CustomCommand extends EditorCommand @getKeymaps: (editor) -> 'meta-V': 'custom1' 'meta-B': 'custom2' @execute: (editor, event) -> + eventName = event @replaceSelectedText editor, (text) -> callbackCount++ text @@ -129,7 +131,9 @@ describe "EditorCommand", -> editor.selectToEndOfLine() editor.trigger 'custom1' expect(callbackCount).toBe 1 + expect(eventName).toBe 'custom1' editor.trigger 'custom2' + expect(eventName).toBe 'custom2' expect(callbackCount).toBe 2 describe "LowerCaseCommand", -> diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee index 22d11adda..426bf65c3 100644 --- a/src/extensions/editor-command.coffee +++ b/src/extensions/editor-command.coffee @@ -18,7 +18,9 @@ class EditorCommand return unless keymaps for key, event of keymaps - editor.on event, => @execute(editor, event) + do (event) => + editor.on event, => + @execute(editor, event) @replaceSelectedText: (editor, replace) -> selection = editor.getSelection() From e87cb34d1d3ce1bfcdbcf19fa962c225d28d2af0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Oct 2012 16:32:55 -0700 Subject: [PATCH 05/83] Support selecting inserted text --- spec/extensions/editor-command-spec.coffee | 19 +++++++++++++++++++ src/app/editor.coffee | 2 +- src/app/selection.coffee | 5 ++++- src/extensions/editor-command.coffee | 8 +++----- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee index eda5d9c7e..da584fb19 100644 --- a/spec/extensions/editor-command-spec.coffee +++ b/spec/extensions/editor-command-spec.coffee @@ -91,6 +91,25 @@ describe "EditorCommand", -> editor.trigger 'devowel' expect(editor.lineForBufferRow(0)).toBe 'vr qcksrt = fnctn () {' expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'vr qcksrt = fnctn () {' + expect(editor.getCursorBufferPosition()).toBe(editor.getSelection().getBufferRange().end) + + it "maintains reversed selections", -> + class VowelRemover extends EditorCommand + @getKeymaps: (editor) -> + 'meta-V': 'devowel' + + @execute: (editor, event) -> + @replaceSelectedText editor, (text) -> + text.replace(/[aeiouy]/gi, '') + + VowelRemover.activate(rootView) + editor.moveCursorToTop() + editor.moveCursorToEndOfLine() + editor.selectToBeginningOfLine() + editor.trigger 'devowel' + expect(editor.lineForBufferRow(0)).toBe 'vr qcksrt = fnctn () {' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'vr qcksrt = fnctn () {' + expect(editor.getCursorBufferPosition()).toBe(editor.getSelection().getBufferRange().start) it "doesn't transform empty selections", -> callbackCount = 0 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 3da6a88a1..41c69752d 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -213,7 +213,7 @@ class Editor extends View deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() deleteLine: -> @activeEditSession.deleteLine() cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() - insertText: (text) -> @activeEditSession.insertText(text) + insertText: (text, options) -> @activeEditSession.insertText(text, options) insertNewline: -> @activeEditSession.insertNewline() insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() indent: -> @activeEditSession.indent() diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 0e0b711f7..b666ce8e5 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -139,7 +139,10 @@ class Selection wasReversed = @isReversed() @clear() newBufferRange = @editSession.buffer.change(oldBufferRange, text) - @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed + if options.select + @setBufferRange(newBufferRange, reverse: wasReversed) + else + @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed autoIndent = options.autoIndent ? true diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee index 426bf65c3..8f2e72ab8 100644 --- a/src/extensions/editor-command.coffee +++ b/src/extensions/editor-command.coffee @@ -26,10 +26,8 @@ class EditorCommand selection = editor.getSelection() return false if selection.isEmpty() - range = selection.getBufferRange() - reverse = selection.isReversed() - text = replace(editor.getTextInRange(range)) + text = replace(editor.getTextInRange(selection.getBufferRange())) return false if text is null or text is undefined - editor.insertText(text) - selection.setBufferRange(range, {reverse}) + + editor.insertText(text, select: true) true From 1fa32c48e713c98a523aeb176057bb140ca89b26 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 4 Oct 2012 17:08:42 -0700 Subject: [PATCH 06/83] Invoke onEditor on each extension sub-class --- spec/extensions/editor-command-spec.coffee | 89 +++++++--------------- src/extensions/editor-command.coffee | 23 ++---- src/extensions/lowercase-command.coffee | 10 +-- src/extensions/uppercase-command.coffee | 10 +-- 4 files changed, 44 insertions(+), 88 deletions(-) diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee index da584fb19..96b2f08ac 100644 --- a/spec/extensions/editor-command-spec.coffee +++ b/spec/extensions/editor-command-spec.coffee @@ -22,13 +22,12 @@ describe "EditorCommand", -> transformed = false edited = false class CustomCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'custom' - @execute: (editor, event) -> - edited = @replaceSelectedText editor, (text) -> - transformed = true - 'new' + @onEditor: (editor) -> + @register editor, 'meta-V', 'custom', => + edited = @replaceSelectedText editor, (text) -> + transformed = true + 'new' CustomCommand.activate(rootView) editor.moveCursorToTop() @@ -41,13 +40,12 @@ describe "EditorCommand", -> transformed = false edited = false class CustomCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'custom' - @execute: (editor, event) -> - edited = @replaceSelectedText editor, (text) -> - transformed = true - null + @onEditor: (editor) -> + @register editor, 'meta-V', 'custom', => + edited = @replaceSelectedText editor, (text) -> + transformed = true + null CustomCommand.activate(rootView) editor.moveCursorToTop() @@ -60,13 +58,12 @@ describe "EditorCommand", -> transformed = false edited = false class CustomCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'custom' - @execute: (editor, event) -> - edited = @replaceSelectedText editor, (text) -> - transformed = true - undefined + @onEditor: (editor) -> + @register editor, 'meta-V', 'custom', => + edited = @replaceSelectedText editor, (text) -> + transformed = true + undefined CustomCommand.activate(rootView) editor.moveCursorToTop() @@ -78,12 +75,11 @@ describe "EditorCommand", -> describe "custom sub-class", -> it "removes vowels from selected text", -> class VowelRemover extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'devowel' - @execute: (editor, event) -> - @replaceSelectedText editor, (text) -> - text.replace(/[aeiouy]/gi, '') + @onEditor: (editor) -> + @register editor, 'meta-V', 'devowel', => + @replaceSelectedText editor, (text) -> + text.replace(/[aeiouy]/gi, '') VowelRemover.activate(rootView) editor.moveCursorToTop() @@ -95,12 +91,10 @@ describe "EditorCommand", -> it "maintains reversed selections", -> class VowelRemover extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'devowel' - - @execute: (editor, event) -> - @replaceSelectedText editor, (text) -> - text.replace(/[aeiouy]/gi, '') + @onEditor: (editor) -> + @register editor, 'meta-V', 'devowel', => + @replaceSelectedText editor, (text) -> + text.replace(/[aeiouy]/gi, '') VowelRemover.activate(rootView) editor.moveCursorToTop() @@ -114,13 +108,11 @@ describe "EditorCommand", -> it "doesn't transform empty selections", -> callbackCount = 0 class CustomCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'custom' - - @execute: (editor, event) -> - @replaceSelectedText editor, (text) -> - callbackCount++ - text + @onEditor: (editor) -> + @register editor, 'meta-V', 'custom', => + @replaceSelectedText editor, (text) -> + callbackCount++ + text CustomCommand.activate(rootView) editor.moveCursorToTop() @@ -131,30 +123,6 @@ describe "EditorCommand", -> editor.trigger 'custom' expect(callbackCount).toBe 1 - it "registers all keymaps", -> - callbackCount = 0 - eventName = null - class CustomCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-V': 'custom1' - 'meta-B': 'custom2' - - @execute: (editor, event) -> - eventName = event - @replaceSelectedText editor, (text) -> - callbackCount++ - text - - CustomCommand.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'custom1' - expect(callbackCount).toBe 1 - expect(eventName).toBe 'custom1' - editor.trigger 'custom2' - expect(eventName).toBe 'custom2' - expect(callbackCount).toBe 2 - describe "LowerCaseCommand", -> it "replaces the selected text with all lower case characters", -> LowerCaseCommand.activate(rootView) @@ -163,7 +131,6 @@ describe "EditorCommand", -> editor.trigger 'lowercase' expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'array' - describe "UpperCaseCommand", -> it "replaces the selected text with all upper case characters", -> UpperCaseCommand.activate(rootView) diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee index 8f2e72ab8..e4a649d07 100644 --- a/src/extensions/editor-command.coffee +++ b/src/extensions/editor-command.coffee @@ -2,25 +2,18 @@ module.exports = class EditorCommand @activate: (rootView) -> - keymaps = @getKeymaps() - return unless keymaps - - window.keymap.bindKeys '.editor', keymaps - for editor in rootView.getEditors() - @subscribeToEditor(rootView, editor) + @onEditor(editor) rootView.on 'editor-open', (e, editor) => - @subscribeToEditor(rootView, editor) + @onEditor(editor) - @subscribeToEditor: (rootView, editor) -> - keymaps = @getKeymaps(rootView, editor) - return unless keymaps - - for key, event of keymaps - do (event) => - editor.on event, => - @execute(editor, event) + @register: (editor, key, event, callback) -> + binding = {} + binding[key] = event + window.keymap.bindKeys '.editor', binding + editor.on event, => + callback(editor, event) @replaceSelectedText: (editor, replace) -> selection = editor.getSelection() diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index 1dd6d7a33..7254fb72d 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -3,9 +3,7 @@ EditorCommand = require 'editor-command' module.exports = class LowerCaseCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-Y': 'lowercase' - - @execute: (editor, event) -> - @replaceSelectedText editor, (text) -> - text.toLowerCase() + @onEditor: (editor) -> + @register editor, 'meta-Y', 'lowercase', => + @replaceSelectedText editor, (text) -> + text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index a79b1969b..2096a91c9 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -3,9 +3,7 @@ EditorCommand = require 'editor-command' module.exports = class UpperCaseCommand extends EditorCommand - @getKeymaps: (editor) -> - 'meta-X': 'uppercase' - - @execute: (editor, event) -> - @replaceSelectedText editor, (text) -> - text.toUpperCase() + @onEditor: (editor) -> + @register editor, 'meta-X', 'uppercase', => + @replaceSelectedText editor, (text) -> + text.toUpperCase() From d4aeb1bb95ede61b91c1cfa46306ca01b0d90444 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Oct 2012 18:41:12 -0700 Subject: [PATCH 07/83] Move EditorCommand helpers elsewhere RootView and Editor now have helpers that support binding events to callbacks, binding a callback to all current and future editors, and replacing the selected text via a transforming callback. --- spec/app/editor-spec.coffee | 52 +++++++ spec/app/root-view-spec.coffee | 28 ++++ spec/extensions/editor-command-spec.coffee | 140 ------------------ spec/extensions/lowercase-command-spec.coffee | 23 +++ spec/extensions/uppercase-command-spec.coffee | 23 +++ src/app/editor.coffee | 17 +++ src/app/root-view.coffee | 7 + src/extensions/editor-command.coffee | 26 ---- src/extensions/lowercase-command.coffee | 11 +- src/extensions/uppercase-command.coffee | 11 +- 10 files changed, 162 insertions(+), 176 deletions(-) delete mode 100644 spec/extensions/editor-command-spec.coffee create mode 100644 spec/extensions/lowercase-command-spec.coffee create mode 100644 spec/extensions/uppercase-command-spec.coffee delete mode 100644 src/extensions/editor-command.coffee diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 93fdc36a7..58643bb91 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1828,3 +1828,55 @@ describe "Editor", -> editor.pageUp() expect(editor.getCursor().getScreenPosition().row).toBe(0) expect(editor.getFirstVisibleScreenRow()).toBe(0) + + describe ".replaceSelectedText()", -> + it "doesn't call the replace function when the selection is empty", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + 'new' + + editor.moveCursorToTop() + edited = editor.replaceSelectedText(replacer) + expect(replaced).toBe false + expect(edited).toBe false + + it "returns true when transformed text is non-empty", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + 'new' + + editor.moveCursorToTop() + editor.selectToEndOfLine() + edited = editor.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe true + + it "returns false when transformed text is null", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + null + + editor.moveCursorToTop() + editor.selectToEndOfLine() + edited = editor.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe false + + it "returns false when transformed text is undefined", -> + replaced = false + edited = false + replacer = (text) -> + replaced = true + undefined + + editor.moveCursorToTop() + editor.selectToEndOfLine() + edited = editor.replaceSelectedText(replacer) + expect(replaced).toBe true + expect(edited).toBe false diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 8e1c2ca81..3b56766a5 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -690,3 +690,31 @@ describe "RootView", -> expect(fs.read(buffer1.getPath())).toBe("edited1") expect(buffer2.isModified()).toBe(false) expect(fs.read(buffer2.getPath())).toBe("edited2") + + fdescribe ".eachEditor(callback)", -> + beforeEach -> + rootView.attachToDom() + + it "invokes the callback for existing editor", -> + count = 0 + callbackEditor = null + callback = (editor) -> + callbackEditor = editor + count++ + rootView.eachEditor(callback) + expect(count).toBe 1 + expect(callbackEditor).toBe rootView.getActiveEditor() + + it "invokes the callback for new editor", -> + count = 0 + callbackEditor = null + callback = (editor) -> + callbackEditor = editor + count++ + + rootView.eachEditor(callback) + count = 0 + callbackEditor = null + rootView.getActiveEditor().splitRight() + expect(count).toBe 1 + expect(callbackEditor).toBe rootView.getActiveEditor() diff --git a/spec/extensions/editor-command-spec.coffee b/spec/extensions/editor-command-spec.coffee deleted file mode 100644 index 96b2f08ac..000000000 --- a/spec/extensions/editor-command-spec.coffee +++ /dev/null @@ -1,140 +0,0 @@ -EditorCommand = require 'editor-command' -LowerCaseCommand = require 'lowercase-command' -UpperCaseCommand = require 'uppercase-command' -RootView = require 'root-view' -fs = require 'fs' - -describe "EditorCommand", -> - [rootView, editor, path] = [] - - beforeEach -> - rootView = new RootView - rootView.open(require.resolve 'fixtures/sample.js') - - rootView.focus() - editor = rootView.getActiveEditor() - - afterEach -> - rootView.remove() - - describe "@replaceSelectedText()", -> - it "returns true when transformed text is non-empty", -> - transformed = false - edited = false - class CustomCommand extends EditorCommand - - @onEditor: (editor) -> - @register editor, 'meta-V', 'custom', => - edited = @replaceSelectedText editor, (text) -> - transformed = true - 'new' - - CustomCommand.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'custom' - expect(transformed).toBe true - expect(edited).toBe true - - it "returns false when transformed text is null", -> - transformed = false - edited = false - class CustomCommand extends EditorCommand - - @onEditor: (editor) -> - @register editor, 'meta-V', 'custom', => - edited = @replaceSelectedText editor, (text) -> - transformed = true - null - - CustomCommand.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'custom' - expect(transformed).toBe true - expect(edited).toBe false - - it "returns false when transformed text is undefined", -> - transformed = false - edited = false - class CustomCommand extends EditorCommand - - @onEditor: (editor) -> - @register editor, 'meta-V', 'custom', => - edited = @replaceSelectedText editor, (text) -> - transformed = true - undefined - - CustomCommand.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'custom' - expect(transformed).toBe true - expect(edited).toBe false - - describe "custom sub-class", -> - it "removes vowels from selected text", -> - class VowelRemover extends EditorCommand - - @onEditor: (editor) -> - @register editor, 'meta-V', 'devowel', => - @replaceSelectedText editor, (text) -> - text.replace(/[aeiouy]/gi, '') - - VowelRemover.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'devowel' - expect(editor.lineForBufferRow(0)).toBe 'vr qcksrt = fnctn () {' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'vr qcksrt = fnctn () {' - expect(editor.getCursorBufferPosition()).toBe(editor.getSelection().getBufferRange().end) - - it "maintains reversed selections", -> - class VowelRemover extends EditorCommand - @onEditor: (editor) -> - @register editor, 'meta-V', 'devowel', => - @replaceSelectedText editor, (text) -> - text.replace(/[aeiouy]/gi, '') - - VowelRemover.activate(rootView) - editor.moveCursorToTop() - editor.moveCursorToEndOfLine() - editor.selectToBeginningOfLine() - editor.trigger 'devowel' - expect(editor.lineForBufferRow(0)).toBe 'vr qcksrt = fnctn () {' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'vr qcksrt = fnctn () {' - expect(editor.getCursorBufferPosition()).toBe(editor.getSelection().getBufferRange().start) - - it "doesn't transform empty selections", -> - callbackCount = 0 - class CustomCommand extends EditorCommand - @onEditor: (editor) -> - @register editor, 'meta-V', 'custom', => - @replaceSelectedText editor, (text) -> - callbackCount++ - text - - CustomCommand.activate(rootView) - editor.moveCursorToTop() - editor.selectToEndOfLine() - editor.trigger 'custom' - expect(callbackCount).toBe 1 - editor.clearSelections() - editor.trigger 'custom' - expect(callbackCount).toBe 1 - - describe "LowerCaseCommand", -> - it "replaces the selected text with all lower case characters", -> - LowerCaseCommand.activate(rootView) - editor.setSelectedBufferRange([[11,14], [11,19]]) - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'Array' - editor.trigger 'lowercase' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'array' - - describe "UpperCaseCommand", -> - it "replaces the selected text with all upper case characters", -> - UpperCaseCommand.activate(rootView) - editor.setSelectedBufferRange([[0,0], [0,3]]) - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'var' - editor.trigger 'uppercase' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'VAR' diff --git a/spec/extensions/lowercase-command-spec.coffee b/spec/extensions/lowercase-command-spec.coffee new file mode 100644 index 000000000..120120f2d --- /dev/null +++ b/spec/extensions/lowercase-command-spec.coffee @@ -0,0 +1,23 @@ +LowerCaseCommand = require 'lowercase-command' +RootView = require 'root-view' +fs = require 'fs' + +describe "LowerCaseCommand", -> + [rootView, editor, path] = [] + + beforeEach -> + rootView = new RootView + rootView.open(require.resolve 'fixtures/sample.js') + + rootView.focus() + editor = rootView.getActiveEditor() + + afterEach -> + rootView.remove() + + it "replaces the selected text with all lower case characters", -> + LowerCaseCommand.activate(rootView) + editor.setSelectedBufferRange([[11,14], [11,19]]) + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'Array' + editor.trigger 'lowercase' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'array' diff --git a/spec/extensions/uppercase-command-spec.coffee b/spec/extensions/uppercase-command-spec.coffee new file mode 100644 index 000000000..5ad77077e --- /dev/null +++ b/spec/extensions/uppercase-command-spec.coffee @@ -0,0 +1,23 @@ +UpperCaseCommand = require 'uppercase-command' +RootView = require 'root-view' +fs = require 'fs' + +describe "UpperCaseCommand", -> + [rootView, editor, path] = [] + + beforeEach -> + rootView = new RootView + rootView.open(require.resolve 'fixtures/sample.js') + + rootView.focus() + editor = rootView.getActiveEditor() + + afterEach -> + rootView.remove() + + it "replaces the selected text with all upper case characters", -> + UpperCaseCommand.activate(rootView) + editor.setSelectedBufferRange([[0,0], [0,3]]) + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'var' + editor.trigger 'uppercase' + expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'VAR' diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 41c69752d..35b752ad3 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -965,3 +965,20 @@ class Editor extends View @find('pre.line.cursor-line').removeClass('cursor-line') if @getSelection().isSingleScreenLine() @find("pre.line:eq(#{screenRow})").addClass('cursor-line') + + bindToKeyedEvent: (key, event, callback) -> + binding = {} + binding[key] = event + window.keymap.bindKeys '.editor', binding + @on event, => + callback(this, event) + + replaceSelectedText: (replaceFn) -> + selection = @getSelection() + return false if selection.isEmpty() + + text = replaceFn(@getTextInRange(selection.getBufferRange())) + return false if text is null or text is undefined + + @insertText(text, select: true) + true diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 38cb4f1e9..4b9479329 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -232,3 +232,10 @@ class RootView extends View saveAll: -> editor.save() for editor in @getEditors() + + eachEditor: (callback) -> + for editor in @getEditors() + callback(editor) + + @on 'editor-open', (e, editor) -> + callback(editor) diff --git a/src/extensions/editor-command.coffee b/src/extensions/editor-command.coffee deleted file mode 100644 index e4a649d07..000000000 --- a/src/extensions/editor-command.coffee +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = -class EditorCommand - - @activate: (rootView) -> - for editor in rootView.getEditors() - @onEditor(editor) - - rootView.on 'editor-open', (e, editor) => - @onEditor(editor) - - @register: (editor, key, event, callback) -> - binding = {} - binding[key] = event - window.keymap.bindKeys '.editor', binding - editor.on event, => - callback(editor, event) - - @replaceSelectedText: (editor, replace) -> - selection = editor.getSelection() - return false if selection.isEmpty() - - text = replace(editor.getTextInRange(selection.getBufferRange())) - return false if text is null or text is undefined - - editor.insertText(text, select: true) - true diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index 7254fb72d..e9fa2c3f4 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -1,9 +1,10 @@ -EditorCommand = require 'editor-command' - module.exports = -class LowerCaseCommand extends EditorCommand +class LowerCaseCommand + + @activate: (rootView) -> + rootView.eachEditor(@onEditor) @onEditor: (editor) -> - @register editor, 'meta-Y', 'lowercase', => - @replaceSelectedText editor, (text) -> + editor.bindToKeyedEvent 'meta-Y', 'lowercase', => + editor.replaceSelectedText (text) -> text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index 2096a91c9..840ee848b 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -1,9 +1,10 @@ -EditorCommand = require 'editor-command' - module.exports = -class UpperCaseCommand extends EditorCommand +class UpperCaseCommand + + @activate: (rootView) -> + rootView.eachEditor(@onEditor) @onEditor: (editor) -> - @register editor, 'meta-X', 'uppercase', => - @replaceSelectedText editor, (text) -> + editor.bindToKeyedEvent 'meta-X', 'uppercase', => + editor.replaceSelectedText (text) -> text.toUpperCase() From e2c7bca3cc4991917930a1fd82a96d15bf8b0b4d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Oct 2012 18:43:56 -0700 Subject: [PATCH 08/83] De-f describe --- spec/app/root-view-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 3b56766a5..bc0d1ec14 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -691,7 +691,7 @@ describe "RootView", -> expect(buffer2.isModified()).toBe(false) expect(fs.read(buffer2.getPath())).toBe("edited2") - fdescribe ".eachEditor(callback)", -> + describe ".eachEditor(callback)", -> beforeEach -> rootView.attachToDom() From 50b1814308211f00218c91ca5e112798c18c850e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Oct 2012 18:45:36 -0700 Subject: [PATCH 09/83] Change => to -> --- src/extensions/lowercase-command.coffee | 2 +- src/extensions/uppercase-command.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index e9fa2c3f4..1b822b1bc 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -5,6 +5,6 @@ class LowerCaseCommand rootView.eachEditor(@onEditor) @onEditor: (editor) -> - editor.bindToKeyedEvent 'meta-Y', 'lowercase', => + editor.bindToKeyedEvent 'meta-Y', 'lowercase', -> editor.replaceSelectedText (text) -> text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index 840ee848b..7d3872a29 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -5,6 +5,6 @@ class UpperCaseCommand rootView.eachEditor(@onEditor) @onEditor: (editor) -> - editor.bindToKeyedEvent 'meta-X', 'uppercase', => + editor.bindToKeyedEvent 'meta-X', 'uppercase', -> editor.replaceSelectedText (text) -> text.toUpperCase() From 24777da703b1df165fa7b1060214f8e6dffe33d7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 10 Oct 2012 19:16:57 -0700 Subject: [PATCH 10/83] Remove unneeded onEditor method --- src/extensions/lowercase-command.coffee | 10 ++++------ src/extensions/uppercase-command.coffee | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/extensions/lowercase-command.coffee b/src/extensions/lowercase-command.coffee index 1b822b1bc..00f04158a 100644 --- a/src/extensions/lowercase-command.coffee +++ b/src/extensions/lowercase-command.coffee @@ -2,9 +2,7 @@ module.exports = class LowerCaseCommand @activate: (rootView) -> - rootView.eachEditor(@onEditor) - - @onEditor: (editor) -> - editor.bindToKeyedEvent 'meta-Y', 'lowercase', -> - editor.replaceSelectedText (text) -> - text.toLowerCase() + rootView.eachEditor (editor) -> + editor.bindToKeyedEvent 'meta-Y', 'lowercase', -> + editor.replaceSelectedText (text) -> + text.toLowerCase() diff --git a/src/extensions/uppercase-command.coffee b/src/extensions/uppercase-command.coffee index 7d3872a29..c7cc02222 100644 --- a/src/extensions/uppercase-command.coffee +++ b/src/extensions/uppercase-command.coffee @@ -2,9 +2,7 @@ module.exports = class UpperCaseCommand @activate: (rootView) -> - rootView.eachEditor(@onEditor) - - @onEditor: (editor) -> - editor.bindToKeyedEvent 'meta-X', 'uppercase', -> - editor.replaceSelectedText (text) -> - text.toUpperCase() + rootView.eachEditor (editor) -> + editor.bindToKeyedEvent 'meta-X', 'uppercase', -> + editor.replaceSelectedText (text) -> + text.toUpperCase() From 220044c8bde971afd51f36de4e237f8a7e9ccba1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 12 Oct 2012 09:16:46 -0700 Subject: [PATCH 11/83] Add eachBuffer helper to RootView This allows extensions to bind a callback to all current and future buffers. --- spec/app/root-view-spec.coffee | 28 +++++++++++++++++++ src/app/root-view.coffee | 7 +++++ .../strip-trailing-whitespace.coffee | 14 +++------- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index bc0d1ec14..6cb214e3f 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -718,3 +718,31 @@ describe "RootView", -> rootView.getActiveEditor().splitRight() expect(count).toBe 1 expect(callbackEditor).toBe rootView.getActiveEditor() + + describe ".eachBuffer(callback)", -> + beforeEach -> + rootView.attachToDom() + + it "invokes the callback for existing buffer", -> + count = 0 + callbackBuffer = null + callback = (buffer) -> + callbackBuffer = buffer + count++ + rootView.eachBuffer(callback) + expect(count).toBe 1 + expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer() + + it "invokes the callback for new buffer", -> + count = 0 + callbackBuffer = null + callback = (buffer) -> + callbackBuffer = buffer + count++ + + rootView.eachBuffer(callback) + count = 0 + callbackBuffer = null + rootView.open(require.resolve('fixtures/sample.txt')) + expect(count).toBe 1 + expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 4b9479329..32a29702c 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -239,3 +239,10 @@ class RootView extends View @on 'editor-open', (e, editor) -> callback(editor) + + eachBuffer: (callback) -> + for buffer in @project.getBuffers() + callback(buffer) + + @project.on 'new-buffer', (buffer) -> + callback(buffer) diff --git a/src/extensions/strip-trailing-whitespace.coffee b/src/extensions/strip-trailing-whitespace.coffee index d33eafe87..cb27068c0 100644 --- a/src/extensions/strip-trailing-whitespace.coffee +++ b/src/extensions/strip-trailing-whitespace.coffee @@ -2,13 +2,7 @@ module.exports = name: "strip trailing whitespace" activate: (rootView) -> - for buffer in rootView.project.getBuffers() - @stripTrailingWhitespaceBeforeSave(buffer) - - rootView.project.on 'new-buffer', (buffer) => - @stripTrailingWhitespaceBeforeSave(buffer) - - stripTrailingWhitespaceBeforeSave: (buffer) -> - buffer.on 'before-save', -> - buffer.scan /[ \t]+$/g, (match, range, { replace }) -> - replace('') + rootView.eachBuffer (buffer) -> + buffer.on 'before-save', -> + buffer.scan /[ \t]+$/g, (match, range, { replace }) -> + replace('') From 361bf8334533889e160ea55efacde126122035bb Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Mon, 7 Jan 2013 13:35:08 -0700 Subject: [PATCH 12/83] Load snippets from CSON/JSON. Use `syntax` properties for scoping. This commit eliminates the custom `snippets` format and instead just uses CSON/JSON. --- .atom/snippets/coffee.cson | 34 ++++++ .atom/snippets/coffee.snippets | 34 ------ src/packages/snippets/snippets.pegjs | 35 +----- .../snippets/spec/snippets-spec.coffee | 111 +++++++----------- src/packages/snippets/src/snippet.coffee | 10 +- src/packages/snippets/src/snippets.coffee | 33 ++++-- 6 files changed, 108 insertions(+), 149 deletions(-) create mode 100644 .atom/snippets/coffee.cson delete mode 100644 .atom/snippets/coffee.snippets diff --git a/.atom/snippets/coffee.cson b/.atom/snippets/coffee.cson new file mode 100644 index 000000000..cf7e223a9 --- /dev/null +++ b/.atom/snippets/coffee.cson @@ -0,0 +1,34 @@ +".source.coffee": + "Describe block": + prefix: "de" + body: """ + describe "${1:description}", -> + ${2:body} + """ + "It block": + prefix: "i" + body: """ + it "$1", -> + $2 + """ + "Before each": + prefix: "be" + body: """ + beforeEach -> + $1 + """ + "Expectation": + prefix: "be" + body: "expect($1).to$2" + "Console log": + prefix: "log" + body: "console.log $1" + "Range array": + prefix: "ra" + body: "[[$1, $2], [$3, $4]]" + "Point array": + prefix: "pt" + body: "[$1, $2]" + "Create Jasmine spy": + prefix: "pt" + body: 'jasmine.createSpy("${1:description}")$2' diff --git a/.atom/snippets/coffee.snippets b/.atom/snippets/coffee.snippets deleted file mode 100644 index 57b3e3980..000000000 --- a/.atom/snippets/coffee.snippets +++ /dev/null @@ -1,34 +0,0 @@ -snippet de "Describe block" -describe "${1:description}", -> - ${2:body} -endsnippet - -snippet i "It block" -it "$1", -> - $2 -endsnippet - -snippet be "Before each" -beforeEach -> - $1 -endsnippet - -snippet ex "Expectation" -expect($1).to$2 -endsnippet - -snippet log "Console log" -console.log $1 -endsnippet - -snippet ra "Range array" -[[$1, $2], [$3, $4]] -endsnippet - -snippet pt "Point array" -[$1, $2] -endsnippet - -snippet spy "Jasmine spy" -jasmine.createSpy("${1:description}")$2 -endsnippet diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/snippets.pegjs index 8d3ea766f..ac132cdd2 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/snippets.pegjs @@ -1,31 +1,10 @@ -{ - var Snippet = require('snippets/src/snippet'); - var Point = require('point'); +body = leadingLines:bodyLineWithNewline* lastLine:bodyLine? { + return lastLine ? leadingLines.concat([lastLine]) : leadingLines; } - -snippets = snippets:snippet+ ws? { - var snippetsByPrefix = {}; - snippets.forEach(function(snippet) { - snippetsByPrefix[snippet.prefix] = snippet - }); - return snippetsByPrefix; -} - -snippet = ws? start ws prefix:prefix ws description:string bodyPosition:beforeBody body:body end { - return new Snippet({ bodyPosition: bodyPosition, prefix: prefix, description: description, body: body }); -} - -start = 'snippet' -prefix = prefix:[A-Za-z0-9_]+ { return prefix.join(''); } -string = ['] body:[^']* ['] { return body.join(''); } - / ["] body:[^"]* ["] { return body.join(''); } - -beforeBody = [ ]* '\n' { return new Point(line, 0); } // return start position of body: body begins on next line, so don't subtract 1 from line - -body = bodyLine+ -bodyLine = content:(tabStop / bodyText)* '\n' { return content; } +bodyLineWithNewline = bodyLine:bodyLine '\n' { return bodyLine; } +bodyLine = content:(tabStop / bodyText)* { return content; } bodyText = text:bodyChar+ { return text.join(''); } -bodyChar = !(end / tabStop) char:[^\n] { return char; } +bodyChar = !(tabStop) char:[^\n] { return char; } tabStop = simpleTabStop / tabStopWithPlaceholder simpleTabStop = '$' index:[0-9]+ { return { index: parseInt(index), placeholderText: '' }; @@ -33,7 +12,3 @@ simpleTabStop = '$' index:[0-9]+ { tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' { return { index: parseInt(index), placeholderText: placeholderText.join('') }; } - -end = 'endsnippet' -ws = ([ \n] / comment)+ -comment = '#' [^\n]* diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 6a1688d4d..e832851e0 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -17,36 +17,45 @@ describe "Snippets extension", -> afterEach -> rootView.remove() + delete window.snippets describe "when 'tab' is triggered on the editor", -> beforeEach -> - Snippets.evalSnippets 'js', """ - snippet t1 "Snippet without tab stops" - this is a test - endsnippet + snippets.add + ".source.js": + "without tab stops": + prefix: "t1" + body: "this is a test" - snippet t2 "With tab stops" - go here next:($2) and finally go here:($3) - go here first:($1) + "tab stops": + prefix: "t2" + body: """ + go here next:($2) and finally go here:($3) + go here first:($1) - endsnippet + """ - snippet t3 "With indented second line" - line 1 - line 2$1 + "indented second line": + prefix: "t3" + body: """ + line 1 + line 2$1 - endsnippet + """ - snippet t4 "With tab stop placeholders" - go here ${1:first} and then here ${2:second} + "tab stop placeholders": + prefix: "t4" + body: """ + go here ${1:first} and then here ${2:second} - endsnippet + """ - snippet t5 "Caused problems with undo" - first line$1 - ${2:placeholder ending second line} - endsnippet - """ + "caused problems with undo": + prefix: "t5" + body: """ + first line$1 + ${2:placeholder ending second line} + """ describe "when the letters preceding the cursor trigger a snippet", -> describe "when the snippet contains no tab stops", -> @@ -188,56 +197,20 @@ describe "Snippets extension", -> anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0]) expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] - describe ".loadSnippetsFile(path)", -> - it "loads the snippets in the given file", -> - spyOn(fs, 'read').andReturn """ - snippet t1 "Test snippet 1" - this is a test 1 - endsnippet - """ - - Snippets.loadSnippetsFile('/tmp/foo/js.snippets') - expect(fs.read).toHaveBeenCalledWith('/tmp/foo/js.snippets') - - editor.insertText("t1") - editor.trigger 'snippets:expand' - expect(buffer.lineForRow(0)).toBe "this is a test 1var quicksort = function () {" - describe "Snippets parser", -> - it "can parse multiple snippets", -> - snippets = Snippets.snippetsParser.parse """ - snippet t1 "Test snippet 1" - this is a test 1 - endsnippet - - snippet t2 "Test snippet 2" - this is a test 2 - endsnippet - """ - expect(_.keys(snippets).length).toBe 2 - snippet = snippets['t1'] - expect(snippet.prefix).toBe 't1' - expect(snippet.description).toBe "Test snippet 1" - expect(snippet.body).toBe "this is a test 1" - - snippet = snippets['t2'] - expect(snippet.prefix).toBe 't2' - expect(snippet.description).toBe "Test snippet 2" - expect(snippet.body).toBe "this is a test 2" - - it "can parse snippets with tabstops", -> - snippets = Snippets.snippetsParser.parse """ - # this line intentially left blank. - snippet t1 "Snippet with tab stops" - go here next:($2) and finally go here:($3) + it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> + bodyTree = Snippets.parser.parse """ + go here next:($2) and finally go here:(${3:here!}) go here first:($1) - endsnippet """ - snippet = snippets['t1'] - expect(snippet.body).toBe """ - go here next:() and finally go here:() - go here first:() - """ - - expect(snippet.tabStops).toEqual [[[1, 15], [1, 15]], [[0, 14], [0, 14]], [[0, 37], [0, 37]]] + expect(bodyTree).toEqual [ + [ + "go here next:(", + { index: 2, placeholderText: "" }, + ") and finally go here:(", + { index: 3, placeholderText: "here!" }, + ")", + ], + [ "go here first:(", { index: 1, placeholderText: "" }, ")"] + ] diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index bcdbd3de5..3d8248d3b 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -3,19 +3,21 @@ Range = require 'range' module.exports = class Snippet + name: null + prefix: null body: null lineCount: null tabStops: null - constructor: ({@bodyPosition, @prefix, @description, body}) -> - @body = @extractTabStops(body) + constructor: ({@name, @prefix, bodyTree}) -> + @body = @extractTabStops(bodyTree) - extractTabStops: (bodyLines) -> + extractTabStops: (bodyTree) -> tabStopsByIndex = {} bodyText = [] [row, column] = [0, 0] - for bodyLine, i in bodyLines + for bodyLine, i in bodyTree lineText = [] for segment in bodyLine if segment.index diff --git a/src/packages/snippets/src/snippets.coffee b/src/packages/snippets/src/snippets.coffee index fc77ddf59..4d88dc0c5 100644 --- a/src/packages/snippets/src/snippets.coffee +++ b/src/packages/snippets/src/snippets.coffee @@ -2,32 +2,41 @@ fs = require 'fs' PEG = require 'pegjs' _ = require 'underscore' SnippetExpansion = require 'snippets/src/snippet-expansion' +Snippet = require './snippet' module.exports = - name: 'Snippets' snippetsByExtension: {} - snippetsParser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true) + parser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true) + userSnippetsDir: fs.join(config.configDirPath, 'snippets') activate: (@rootView) -> - @loadSnippets() + window.snippets = this + @loadAll() @rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor) - loadSnippets: -> - snippetsDir = fs.join(config.configDirPath, 'snippets') - if fs.exists(snippetsDir) - @loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets' + loadAll: -> + for snippetsPath in fs.list(@userSnippetsDir) + @load(snippetsPath) - loadSnippetsFile: (path) -> - @evalSnippets(fs.base(path, '.snippets'), fs.read(path)) + load: (snippetsPath) -> + @add(fs.readObject(snippetsPath)) + + add: (snippetsBySelector) -> + for selector, snippetsByName of snippetsBySelector + snippetsByPrefix = {} + for name, attributes of snippetsByName + { prefix, body } = attributes + bodyTree = @parser.parse(body) + snippet = new Snippet({name, prefix, bodyTree}) + snippetsByPrefix[snippet.prefix] = snippet + syntax.addProperties(selector, snippets: snippetsByPrefix) - evalSnippets: (extension, text) -> - @snippetsByExtension[extension] = @snippetsParser.parse(text) enableSnippetsInEditor: (editor) -> editor.command 'snippets:expand', (e) => editSession = editor.activeEditSession prefix = editSession.getLastCursor().getCurrentWordPrefix() - if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix] + if snippet = syntax.getProperty(editSession.getCursorScopes(), "snippets.#{prefix}") editSession.transact -> snippetExpansion = new SnippetExpansion(snippet, editSession) editSession.snippetExpansion = snippetExpansion From 6efe533650df9b75a62c402165d0bc0182286fe0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Mon, 7 Jan 2013 14:25:56 -0700 Subject: [PATCH 13/83] Add `atom.getPackages` so we can access package objects anywhere --- spec/spec-helper.coffee | 2 +- src/app/atom.coffee | 30 ++++++++++++++++++------------ src/app/package.coffee | 8 +++----- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c7dc053ca..f6a4df0eb 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -17,7 +17,7 @@ require.paths.unshift(require.resolve('fixtures/packages')) [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = [] # Load TextMate bundles, which specs rely on (but not other packages) -atom.loadPackages(atom.getAvailableTextMateBundles()) +atom.loadTextMatePackages() beforeEach -> window.fixturesProject = new Project(require.resolve('fixtures')) diff --git a/src/app/atom.coffee b/src/app/atom.coffee index a90b675b3..96594961e 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -12,7 +12,23 @@ _.extend atom, pendingBrowserProcessCallbacks: {} - getAvailablePackages: -> + loadPackages: -> + pack.load() for pack in @getPackages() + + getPackages: -> + @getPackageNames().map (name) -> Package.build(name) + + loadTextMatePackages: -> + pack.load() for pack in @getTextMatePackages() + + getTextMatePackages: -> + @getPackages().filter (pack) -> pack instanceof TextMatePackage + + loadPackage: (name) -> + Package.build(name).load() + + getPackageNames: -> + disabledPackages = config.get("core.disabledPackages") ? [] allPackageNames = [] for packageDirPath in config.packageDirPaths packageNames = fs.list(packageDirPath) @@ -20,17 +36,7 @@ _.extend atom, .map((packagePath) -> fs.base(packagePath)) allPackageNames.push(packageNames...) _.unique(allPackageNames) - - getAvailableTextMateBundles: -> - @getAvailablePackages().filter (packageName) => TextMatePackage.testName(packageName) - - loadPackages: (packageNames=@getAvailablePackages()) -> - disabledPackages = config.get("core.disabledPackages") ? [] - for packageName in packageNames - @loadPackage(packageName) unless _.contains(disabledPackages, packageName) - - loadPackage: (name) -> - Package.load(name) + .filter (name) -> not _.contains(disabledPackages, name) loadThemes: -> themeNames = config.get("core.themes") ? ['IR_Black'] diff --git a/src/app/package.coffee b/src/app/package.coffee index 49d6445ac..1f1cb6c68 100644 --- a/src/app/package.coffee +++ b/src/app/package.coffee @@ -2,15 +2,13 @@ fs = require 'fs' module.exports = class Package - @load: (name) -> + @build: (name) -> AtomPackage = require 'atom-package' TextMatePackage = require 'text-mate-package' - if TextMatePackage.testName(name) - new TextMatePackage(name).load() + new TextMatePackage(name) else - new AtomPackage(name).load() - + new AtomPackage(name) name: null path: null From f008ff52e8952e598254f1d67ea01484872fc51b Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Mon, 7 Jan 2013 14:26:27 -0700 Subject: [PATCH 14/83] Load snippets from any atom package with a `snippets` directory --- .atom/snippets/coffee.cson | 8 +++++++- .../packages/package-with-snippets/snippets/test.cson | 4 ++++ spec/spec-helper.coffee | 4 +++- src/packages/snippets/spec/snippets-spec.coffee | 5 +++++ src/packages/snippets/src/package-extensions.coffee | 11 +++++++++++ src/packages/snippets/src/snippets.coffee | 4 ++++ 6 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/packages/package-with-snippets/snippets/test.cson create mode 100644 src/packages/snippets/src/package-extensions.coffee diff --git a/.atom/snippets/coffee.cson b/.atom/snippets/coffee.cson index cf7e223a9..f960ce1a5 100644 --- a/.atom/snippets/coffee.cson +++ b/.atom/snippets/coffee.cson @@ -17,8 +17,14 @@ beforeEach -> $1 """ + "After each": + prefix: "af" + body: """ + afterEach -> + $1 + """ "Expectation": - prefix: "be" + prefix: "ex" body: "expect($1).to$2" "Console log": prefix: "log" diff --git a/spec/fixtures/packages/package-with-snippets/snippets/test.cson b/spec/fixtures/packages/package-with-snippets/snippets/test.cson new file mode 100644 index 000000000..b936fea16 --- /dev/null +++ b/spec/fixtures/packages/package-with-snippets/snippets/test.cson @@ -0,0 +1,4 @@ +".test": + "Test Snippet": + prefix: "test" + body: "testing 123" diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index f6a4df0eb..c15eb37b2 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -13,7 +13,8 @@ TokenizedBuffer = require 'tokenized-buffer' fs = require 'fs' require 'window' requireStylesheet "jasmine.css" -require.paths.unshift(require.resolve('fixtures/packages')) +fixturePackagesPath = require.resolve('fixtures/packages') +require.paths.unshift(fixturePackagesPath) [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = [] # Load TextMate bundles, which specs rely on (but not other packages) @@ -29,6 +30,7 @@ beforeEach -> # reset config before each spec; don't load or save from/to `config.json` window.config = new Config() + config.packageDirPaths.unshift(fixturePackagesPath) spyOn(config, 'load') spyOn(config, 'save') config.set "editor.fontSize", 16 diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index e832851e0..f1b262679 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -1,4 +1,5 @@ Snippets = require 'snippets' +Snippet = require 'snippets/src/snippet' RootView = require 'root-view' Buffer = require 'buffer' Editor = require 'editor' @@ -197,6 +198,10 @@ describe "Snippets extension", -> anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0]) expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] + describe "snippet loading", -> + it "loads snippets from all packages with a snippets directory", -> + expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet + describe "Snippets parser", -> it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> bodyTree = Snippets.parser.parse """ diff --git a/src/packages/snippets/src/package-extensions.coffee b/src/packages/snippets/src/package-extensions.coffee new file mode 100644 index 000000000..f4de953f7 --- /dev/null +++ b/src/packages/snippets/src/package-extensions.coffee @@ -0,0 +1,11 @@ +AtomPackage = require 'atom-package' +TextMatePackage = require 'text-mate-package' +fs = require 'fs' + +AtomPackage.prototype.loadSnippets = -> + snippetsDirPath = fs.join(@path, 'snippets') + if fs.exists(snippetsDirPath) + for snippetsPath in fs.list(snippetsDirPath) + snippets.load(snippetsPath) + +TextMatePackage.prototype.loadSnippets = -> diff --git a/src/packages/snippets/src/snippets.coffee b/src/packages/snippets/src/snippets.coffee index 4d88dc0c5..3d7a35a48 100644 --- a/src/packages/snippets/src/snippets.coffee +++ b/src/packages/snippets/src/snippets.coffee @@ -3,6 +3,7 @@ PEG = require 'pegjs' _ = require 'underscore' SnippetExpansion = require 'snippets/src/snippet-expansion' Snippet = require './snippet' +require './package-extensions' module.exports = snippetsByExtension: {} @@ -15,6 +16,9 @@ module.exports = @rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor) loadAll: -> + for pack in atom.getPackages() + pack.loadSnippets() + for snippetsPath in fs.list(@userSnippetsDir) @load(snippetsPath) From 858ad69484d4098389a9f68b846c5a0df959566c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Jan 2013 16:09:18 -0700 Subject: [PATCH 15/83] Simplify snippet body parsing Previously, we parsed snippet bodies line at a time, then determined tab stops within lines. But this disallows tab stops with placeholder text that spans multiple lines. Now the parser produces a simpler structure that breaks the body into an array of strings and tab stops. Newlines are represented directly as characters within the strings. --- src/packages/snippets/snippets.pegjs | 8 ++--- .../snippets/spec/snippets-spec.coffee | 17 +++++------ src/packages/snippets/src/snippet.coffee | 29 ++++++++++--------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/snippets.pegjs index ac132cdd2..7719df9cc 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/snippets.pegjs @@ -1,10 +1,6 @@ -body = leadingLines:bodyLineWithNewline* lastLine:bodyLine? { - return lastLine ? leadingLines.concat([lastLine]) : leadingLines; -} -bodyLineWithNewline = bodyLine:bodyLine '\n' { return bodyLine; } -bodyLine = content:(tabStop / bodyText)* { return content; } +body = content:(tabStop / bodyText)* { return content; } bodyText = text:bodyChar+ { return text.join(''); } -bodyChar = !(tabStop) char:[^\n] { return char; } +bodyChar = !tabStop char:. { return char; } tabStop = simpleTabStop / tabStopWithPlaceholder simpleTabStop = '$' index:[0-9]+ { return { index: parseInt(index), placeholderText: '' }; diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index f1b262679..bd1683625 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -199,7 +199,7 @@ describe "Snippets extension", -> expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] describe "snippet loading", -> - it "loads snippets from all packages with a snippets directory", -> + it "loads snippets from all atom packages with a snippets directory", -> expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet describe "Snippets parser", -> @@ -210,12 +210,11 @@ describe "Snippets extension", -> """ expect(bodyTree).toEqual [ - [ - "go here next:(", - { index: 2, placeholderText: "" }, - ") and finally go here:(", - { index: 3, placeholderText: "here!" }, - ")", - ], - [ "go here first:(", { index: 1, placeholderText: "" }, ")"] + "go here next:(", + { index: 2, placeholderText: "" }, + ") and finally go here:(", + { index: 3, placeholderText: "here!" }, + ")\ngo here first:(", + { index: 1, placeholderText: "" }, + ")" ] diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index 3d8248d3b..22ac3be94 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -17,22 +17,23 @@ class Snippet bodyText = [] [row, column] = [0, 0] - for bodyLine, i in bodyTree - lineText = [] - for segment in bodyLine - if segment.index - { index, placeholderText } = segment - tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length]) - lineText.push(placeholderText) - else - lineText.push(segment) - column += segment.length - bodyText.push(lineText.join('')) - row++; column = 0 - @lineCount = row + for segment in bodyTree + if segment.index + { index, placeholderText } = segment + tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length]) + bodyText.push(placeholderText) + column += placeholderText.length + else + bodyText.push(segment) + segmentLines = segment.split('\n') + column += segmentLines.shift().length + while nextLine = segmentLines.shift() + row += 1 + column = nextLine.length + @lineCount = row + 1 @tabStops = [] for index in _.keys(tabStopsByIndex).sort() @tabStops.push tabStopsByIndex[index] - bodyText.join('\n') + bodyText.join('') From 60c89f8b32a385904687407596370519dc526bf6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Jan 2013 16:35:11 -0700 Subject: [PATCH 16/83] Allow snippet tab stop placeholder text to contain newlines --- src/packages/snippets/snippets.pegjs | 6 ++-- .../snippets/spec/snippets-spec.coffee | 33 ++++++++++++------- src/packages/snippets/src/snippet.coffee | 31 +++++++++-------- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/snippets.pegjs index 7719df9cc..aeda595bc 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/snippets.pegjs @@ -3,8 +3,8 @@ bodyText = text:bodyChar+ { return text.join(''); } bodyChar = !tabStop char:. { return char; } tabStop = simpleTabStop / tabStopWithPlaceholder simpleTabStop = '$' index:[0-9]+ { - return { index: parseInt(index), placeholderText: '' }; + return { index: parseInt(index), content: [] }; } -tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' { - return { index: parseInt(index), placeholderText: placeholderText.join('') }; +tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:[^}]* '}' { + return { index: parseInt(index), content: [content.join('')] }; } diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index bd1683625..b8223599f 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -47,12 +47,20 @@ describe "Snippets extension", -> "tab stop placeholders": prefix: "t4" body: """ - go here ${1:first} and then here ${2:second} + go here ${1:first + think a while}, and then here ${2:second} """ - "caused problems with undo": + "multi-line placeholders": prefix: "t5" + body: """ + behold ${1:my multi- + line placeholder}. amazing. + """ + + "caused problems with undo": + prefix: "t6" body: """ first line$1 ${2:placeholder ending second line} @@ -108,8 +116,11 @@ describe "Snippets extension", -> it "auto-fills the placeholder text and highlights it when navigating to that tab stop", -> editor.insertText 't4' editor.trigger 'snippets:expand' - expect(buffer.lineForRow(0)).toBe 'go here first and then here second' - expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [0, 13]] + expect(buffer.lineForRow(0)).toBe 'go here first' + expect(buffer.lineForRow(1)).toBe 'think a while, and then here second' + expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [1, 13]] + editor.trigger keydownEvent('tab', target: editor[0]) + expect(editor.getSelectedBufferRange()).toEqual [[1, 29], [1, 35]] describe "when the cursor is moved beyond the bounds of a tab stop", -> it "terminates the snippet", -> @@ -162,18 +173,18 @@ describe "Snippets extension", -> describe "when a previous snippet expansion has just been undone", -> it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", -> - editor.insertText 't5\n' + editor.insertText 't6\n' editor.setCursorBufferPosition [0, 2] editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe "first line" editor.undo() - expect(buffer.lineForRow(0)).toBe "t5" + expect(buffer.lineForRow(0)).toBe "t6" editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe "first line" describe "when a snippet expansion is undone and redone", -> it "recreates the snippet's tab stops", -> - editor.insertText ' t5\n' + editor.insertText ' t6\n' editor.setCursorBufferPosition [0, 6] editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe " first line" @@ -187,7 +198,7 @@ describe "Snippets extension", -> it "restores tabs stops in active edit session even when the initial expansion was in a different edit session", -> anotherEditor = editor.splitRight() - editor.insertText ' t5\n' + editor.insertText ' t6\n' editor.setCursorBufferPosition [0, 6] editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe " first line" @@ -211,10 +222,10 @@ describe "Snippets extension", -> expect(bodyTree).toEqual [ "go here next:(", - { index: 2, placeholderText: "" }, + { index: 2, content: [] }, ") and finally go here:(", - { index: 3, placeholderText: "here!" }, + { index: 3, content: ["here!"] }, ")\ngo here first:(", - { index: 1, placeholderText: "" }, + { index: 1, content: [] }, ")" ] diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index 22ac3be94..b55923d38 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -15,22 +15,25 @@ class Snippet extractTabStops: (bodyTree) -> tabStopsByIndex = {} bodyText = [] - [row, column] = [0, 0] - for segment in bodyTree - if segment.index - { index, placeholderText } = segment - tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length]) - bodyText.push(placeholderText) - column += placeholderText.length - else - bodyText.push(segment) - segmentLines = segment.split('\n') - column += segmentLines.shift().length - while nextLine = segmentLines.shift() - row += 1 - column = nextLine.length + # recursive helper function; mutates vars above + extractTabStops = (bodyTree) -> + for segment in bodyTree + if segment.index + { index, content } = segment + start = [row, column] + extractTabStops(content) + tabStopsByIndex[index] = new Range(start, [row, column]) + else + bodyText.push(segment) + segmentLines = segment.split('\n') + column += segmentLines.shift().length + while nextLine = segmentLines.shift() + row += 1 + column = nextLine.length + + extractTabStops(bodyTree) @lineCount = row + 1 @tabStops = [] for index in _.keys(tabStopsByIndex).sort() From 62d72730699f8fb54c18aed73d73327f824dd26a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Jan 2013 16:49:48 -0700 Subject: [PATCH 17/83] Parse nested snippet placeholders --- src/packages/snippets/snippets.pegjs | 15 ++++++++---- .../snippets/spec/snippets-spec.coffee | 23 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/snippets.pegjs index aeda595bc..72ec9be48 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/snippets.pegjs @@ -1,10 +1,15 @@ -body = content:(tabStop / bodyText)* { return content; } -bodyText = text:bodyChar+ { return text.join(''); } -bodyChar = !tabStop char:. { return char; } +bodyContent = content:(tabStop / bodyContentText)* { return content; } +bodyContentText = text:bodyContentChar+ { return text.join(''); } +bodyContentChar = !tabStop char:. { return char; } + +placeholderContent = content:(tabStop / placeholderContentText)* { return content; } +placeholderContentText = text:placeholderContentChar+ { return text.join(''); } +placeholderContentChar = !tabStop char:[^}] { return char; } + tabStop = simpleTabStop / tabStopWithPlaceholder simpleTabStop = '$' index:[0-9]+ { return { index: parseInt(index), content: [] }; } -tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:[^}]* '}' { - return { index: parseInt(index), content: [content.join('')] }; +tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' { + return { index: parseInt(index), content: content }; } diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index b8223599f..53598e1fe 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -216,16 +216,23 @@ describe "Snippets extension", -> describe "Snippets parser", -> it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> bodyTree = Snippets.parser.parse """ - go here next:($2) and finally go here:(${3:here!}) - go here first:($1) + the quick brown $1fox ${2:jumped ${3:over} + }the ${4:lazy} dog """ expect(bodyTree).toEqual [ - "go here next:(", - { index: 2, content: [] }, - ") and finally go here:(", - { index: 3, content: ["here!"] }, - ")\ngo here first:(", + "the quick brown ", { index: 1, content: [] }, - ")" + "fox ", + { + index: 2, + content: [ + "jumped ", + { index: 3, content: ["over"]}, + "\n" + ], + } + "the " + { index: 4, content: ["lazy"] }, + " dog" ] From 314e3da8bc1ffee72aeb005bde6c9f2f66136a21 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Jan 2013 21:46:44 -0700 Subject: [PATCH 18/83] WIP: Destroy nested tab stops when engulfed by a buffer change Has 2 failing specs... There are still some issue with this code's interaction with the undo system. The tab stops will need to be or destroyed when certain changes are undone or redone. --- .atom/snippets/coffee.cson | 4 ++++ src/app/anchor-range.coffee | 13 ++++++++++++ src/app/anchor.coffee | 3 +++ .../snippets/spec/snippets-spec.coffee | 20 +++++++++++++------ .../snippets/src/snippet-expansion.coffee | 6 ++++-- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.atom/snippets/coffee.cson b/.atom/snippets/coffee.cson index f960ce1a5..ff978671c 100644 --- a/.atom/snippets/coffee.cson +++ b/.atom/snippets/coffee.cson @@ -35,6 +35,10 @@ "Point array": prefix: "pt" body: "[$1, $2]" + + "Key-value pair": + prefix: ":" + body: '${1:"${2:key}"}: ${3:value}' "Create Jasmine spy": prefix: "pt" body: 'jasmine.createSpy("${1:description}")$2' diff --git a/src/app/anchor-range.coffee b/src/app/anchor-range.coffee index 7adfa1cf2..1f1ad5151 100644 --- a/src/app/anchor-range.coffee +++ b/src/app/anchor-range.coffee @@ -1,4 +1,7 @@ Range = require 'range' +EventEmitter = require 'event-emitter' +Subscriber = require 'subscriber' +_ = require 'underscore' module.exports = class AnchorRange @@ -6,11 +9,14 @@ class AnchorRange end: null buffer: null editSession: null # optional + destroyed: false constructor: (bufferRange, @buffer, @editSession) -> bufferRange = Range.fromObject(bufferRange) @startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true) @endAnchor = @buffer.addAnchorAtPosition(bufferRange.end) + @subscribe @startAnchor, 'destroyed', => @destroy() + @subscribe @endAnchor, 'destroyed', => @destroy() getBufferRange: -> new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition()) @@ -22,7 +28,14 @@ class AnchorRange @getBufferRange().containsPoint(bufferPosition) destroy: -> + return if @destroyed + @unsubscribe() @startAnchor.destroy() @endAnchor.destroy() @buffer.removeAnchorRange(this) @editSession?.removeAnchorRange(this) + @destroyed = true + @trigger 'destroyed' + +_.extend(AnchorRange.prototype, EventEmitter) +_.extend(AnchorRange.prototype, Subscriber) diff --git a/src/app/anchor.coffee b/src/app/anchor.coffee index 42932c97c..f0b26b10b 100644 --- a/src/app/anchor.coffee +++ b/src/app/anchor.coffee @@ -10,6 +10,7 @@ class Anchor screenPosition: null ignoreChangesStartingOnAnchor: false strong: false + destroyed: false constructor: (@buffer, options = {}) -> { @editSession, @ignoreChangesStartingOnAnchor, @strong } = options @@ -81,8 +82,10 @@ class Anchor @setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll) destroy: -> + return if @destroyed @buffer.removeAnchor(this) @editSession?.removeAnchor(this) + @destroyed = true @trigger 'destroyed' _.extend(Anchor.prototype, EventEmitter) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 53598e1fe..38a711489 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -52,12 +52,9 @@ describe "Snippets extension", -> """ - "multi-line placeholders": + "nested tab stops": prefix: "t5" - body: """ - behold ${1:my multi- - line placeholder}. amazing. - """ + body: '${1:"${2:key}"}: ${3:value}' "caused problems with undo": prefix: "t6" @@ -122,6 +119,17 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', target: editor[0]) expect(editor.getSelectedBufferRange()).toEqual [[1, 29], [1, 35]] + describe "when tab stops are nested", -> + it "destroys the inner tab stop if the outer tab stop is modified", -> + buffer.setText('') + editor.insertText 't5' + editor.trigger 'snippets:expand' + expect(buffer.lineForRow(0)).toBe '"key": value' + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 5]] + editor.insertText("foo") + editor.trigger keydownEvent('tab', target: editor[0]) + expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]] + describe "when the cursor is moved beyond the bounds of a tab stop", -> it "terminates the snippet", -> editor.setCursorScreenPosition([2, 0]) @@ -182,7 +190,7 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe "first line" - describe "when a snippet expansion is undone and redone", -> + ffdescribe "when a snippet expansion is undone and redone", -> it "recreates the snippet's tab stops", -> editor.insertText ' t6\n' editor.setCursorBufferPosition [0, 6] diff --git a/src/packages/snippets/src/snippet-expansion.coffee b/src/packages/snippets/src/snippet-expansion.coffee index b0e80f83d..dd7a5d11f 100644 --- a/src/packages/snippets/src/snippet-expansion.coffee +++ b/src/packages/snippets/src/snippet-expansion.coffee @@ -25,7 +25,9 @@ class SnippetExpansion placeTabStopAnchorRanges: (startPosition, tabStopRanges) -> @tabStopAnchorRanges = tabStopRanges.map ({start, end}) => - @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)]) + anchorRange = @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)]) + anchorRange.on 'destroyed', => _.remove(@tabStopAnchorRanges, anchorRange) + anchorRange @setTabStopIndex(0) indentSubsequentLines: (startRow, snippet) -> @@ -68,7 +70,7 @@ class SnippetExpansion _.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition)) destroy: -> - anchorRange.destroy() for anchorRange in @tabStopAnchorRanges + anchorRange.destroy() for anchorRange in new Array(@tabStopAnchorRanges...) @editSession.off '.snippet-expansion' @editSession.snippetExpansion = null From a03bb7bf2e51211e0cb456b0ce984ab96a96224b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Jan 2013 12:53:43 -0700 Subject: [PATCH 19/83] Un-f --- src/packages/snippets/spec/snippets-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 38a711489..718ea0aca 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -190,7 +190,7 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe "first line" - ffdescribe "when a snippet expansion is undone and redone", -> + describe "when a snippet expansion is undone and redone", -> it "recreates the snippet's tab stops", -> editor.insertText ' t6\n' editor.setCursorBufferPosition [0, 6] From cab5b25e76f8e6048574f8b9a3c769ffdeec8f7a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Jan 2013 12:54:49 -0700 Subject: [PATCH 20/83] Fix undo/redo of snippet expansions. Tab stops are restored correctly. We're giving up on correctly restoring snippet expansions that occurred in a different EditSession. --- .../snippets/spec/snippets-spec.coffee | 15 -------- .../snippets/src/snippet-expansion.coffee | 37 ++++++++++++------- src/packages/snippets/src/snippets.coffee | 6 +-- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 718ea0aca..29f96ad03 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -198,25 +198,10 @@ describe "Snippets extension", -> expect(buffer.lineForRow(0)).toBe " first line" editor.undo() editor.redo() - expect(editor.getCursorBufferPosition()).toEqual [0, 14] editor.trigger keydownEvent('tab', target: editor[0]) expect(editor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] - it "restores tabs stops in active edit session even when the initial expansion was in a different edit session", -> - anotherEditor = editor.splitRight() - - editor.insertText ' t6\n' - editor.setCursorBufferPosition [0, 6] - editor.trigger keydownEvent('tab', target: editor[0]) - expect(buffer.lineForRow(0)).toBe " first line" - editor.undo() - - anotherEditor.redo() - expect(anotherEditor.getCursorBufferPosition()).toEqual [0, 14] - anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0]) - expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] - describe "snippet loading", -> it "loads snippets from all atom packages with a snippets directory", -> expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet diff --git a/src/packages/snippets/src/snippet-expansion.coffee b/src/packages/snippets/src/snippet-expansion.coffee index dd7a5d11f..74dcd5d6f 100644 --- a/src/packages/snippets/src/snippet-expansion.coffee +++ b/src/packages/snippets/src/snippet-expansion.coffee @@ -1,35 +1,44 @@ +Subscriber = require 'subscriber' _ = require 'underscore' module.exports = class SnippetExpansion + snippet: null tabStopAnchorRanges: null settingTabStop: false - constructor: (snippet, @editSession) -> + constructor: (@snippet, @editSession) -> @editSession.selectToBeginningOfWord() startPosition = @editSession.getCursorBufferPosition() @editSession.transact => @editSession.insertText(snippet.body, autoIndent: false) - if snippet.tabStops.length - @placeTabStopAnchorRanges(startPosition, snippet.tabStops) - if snippet.lineCount > 1 - @indentSubsequentLines(startPosition.row, snippet) + editSession.pushOperation + do: => + @subscribe @editSession, 'cursor-moved.snippet-expansion', (e) => @cursorMoved(e) + @placeTabStopAnchorRanges(startPosition, snippet.tabStops) + @editSession.snippetExpansion = this + undo: => @destroy() + @indentSubsequentLines(startPosition.row, snippet) if snippet.lineCount > 1 - @editSession.on 'cursor-moved.snippet-expansion', ({oldBufferPosition, newBufferPosition}) => - return if @settingTabStop + cursorMoved: ({oldBufferPosition, newBufferPosition}) -> + return if @settingTabStop - oldTabStops = @tabStopsForBufferPosition(oldBufferPosition) - newTabStops = @tabStopsForBufferPosition(newBufferPosition) + oldTabStops = @tabStopsForBufferPosition(oldBufferPosition) + newTabStops = @tabStopsForBufferPosition(newBufferPosition) - @destroy() unless _.intersect(oldTabStops, newTabStops).length + @destroy() unless _.intersect(oldTabStops, newTabStops).length placeTabStopAnchorRanges: (startPosition, tabStopRanges) -> + return unless @snippet.tabStops.length > 0 + @tabStopAnchorRanges = tabStopRanges.map ({start, end}) => anchorRange = @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)]) - anchorRange.on 'destroyed', => _.remove(@tabStopAnchorRanges, anchorRange) + @subscribe anchorRange, 'destroyed', => + _.remove(@tabStopAnchorRanges, anchorRange) anchorRange @setTabStopIndex(0) + indentSubsequentLines: (startRow, snippet) -> initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0] for row in [startRow + 1...startRow + snippet.lineCount] @@ -70,11 +79,13 @@ class SnippetExpansion _.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition)) destroy: -> - anchorRange.destroy() for anchorRange in new Array(@tabStopAnchorRanges...) - @editSession.off '.snippet-expansion' + @unsubscribe() + anchorRange.destroy() for anchorRange in @tabStopAnchorRanges @editSession.snippetExpansion = null restore: (@editSession) -> @editSession.snippetExpansion = this @tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) => @editSession.addAnchorRange(anchorRange.getBufferRange()) + +_.extend(SnippetExpansion.prototype, Subscriber) diff --git a/src/packages/snippets/src/snippets.coffee b/src/packages/snippets/src/snippets.coffee index 3d7a35a48..5ff21f0b7 100644 --- a/src/packages/snippets/src/snippets.coffee +++ b/src/packages/snippets/src/snippets.coffee @@ -42,11 +42,7 @@ module.exports = prefix = editSession.getLastCursor().getCurrentWordPrefix() if snippet = syntax.getProperty(editSession.getCursorScopes(), "snippets.#{prefix}") editSession.transact -> - snippetExpansion = new SnippetExpansion(snippet, editSession) - editSession.snippetExpansion = snippetExpansion - editSession.pushOperation - undo: -> snippetExpansion.destroy() - redo: (editSession) -> snippetExpansion.restore(editSession) + new SnippetExpansion(snippet, editSession) else e.abortKeyBinding() From 2d11239ac1d9167bf17477d0cb00b35ee5c580e7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 10:03:53 -0800 Subject: [PATCH 21/83] Stub out more docs --- .github | 2 +- docs/extensions/intro.md | 1 - docs/features.md | 1 + docs/intro.md | 2 +- docs/packages/adding.md | 1 + docs/packages/intro.md | 21 +++++++++++++++++++ .../markdown-preview.md | 0 docs/{extensions => packages}/wrap-guide.md | 0 8 files changed, 25 insertions(+), 3 deletions(-) delete mode 100644 docs/extensions/intro.md create mode 100644 docs/features.md create mode 100644 docs/packages/adding.md create mode 100644 docs/packages/intro.md rename docs/{extensions => packages}/markdown-preview.md (100%) rename docs/{extensions => packages}/wrap-guide.md (100%) diff --git a/.github b/.github index cca25cd49..083168644 100644 --- a/.github +++ b/.github @@ -1,3 +1,3 @@ [docs] title = The Guide to Atom - manifest = intro.md, configuring-and-extending.md, styling.md, extensions/intro.md, extensions/markdown-preview.md, extensions/wrap-guide.md + manifest = intro.md, features.md, configuring-and-extending.md, styling.md, packages/intro.md, packages/adding.md, packages/markdown-preview.md, packages/wrap-guide.md diff --git a/docs/extensions/intro.md b/docs/extensions/intro.md deleted file mode 100644 index 01c1c040e..000000000 --- a/docs/extensions/intro.md +++ /dev/null @@ -1 +0,0 @@ -## Extensions diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 000000000..304a5d833 --- /dev/null +++ b/docs/features.md @@ -0,0 +1 @@ +## Features diff --git a/docs/intro.md b/docs/intro.md index 402cf5a27..32a838246 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1,3 +1,3 @@ -## The Definitive Guide to Atom +# The Atom Guide Welcome! diff --git a/docs/packages/adding.md b/docs/packages/adding.md new file mode 100644 index 000000000..803454868 --- /dev/null +++ b/docs/packages/adding.md @@ -0,0 +1 @@ +## Adding a package diff --git a/docs/packages/intro.md b/docs/packages/intro.md new file mode 100644 index 000000000..60c29a983 --- /dev/null +++ b/docs/packages/intro.md @@ -0,0 +1,21 @@ +## Packages + +Atom comes with several built-in packages that add features to the default +editor. + +The current built-in packages are: + + * Autocomplete + * Command Logger + * Command Palette + * Fuzzy finder + * Markdown Preview + * Outline View + * Snippets + * Status Bar + * Strip Trailing Whitespace + * Tabs + * Tree View + * Wrap Guide + +### Package Layout diff --git a/docs/extensions/markdown-preview.md b/docs/packages/markdown-preview.md similarity index 100% rename from docs/extensions/markdown-preview.md rename to docs/packages/markdown-preview.md diff --git a/docs/extensions/wrap-guide.md b/docs/packages/wrap-guide.md similarity index 100% rename from docs/extensions/wrap-guide.md rename to docs/packages/wrap-guide.md From 077310b80c98278226f099b1247146033cd85d31 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 10:17:39 -0800 Subject: [PATCH 22/83] Put layout before includes and link to package docs --- docs/packages/intro.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/packages/intro.md b/docs/packages/intro.md index 60c29a983..8cae71ad6 100644 --- a/docs/packages/intro.md +++ b/docs/packages/intro.md @@ -1,5 +1,9 @@ ## Packages +### Package Layout + +### Included Packages + Atom comes with several built-in packages that add features to the default editor. @@ -9,13 +13,11 @@ The current built-in packages are: * Command Logger * Command Palette * Fuzzy finder - * Markdown Preview + * [Markdown Preview](#markdown-preview) * Outline View * Snippets * Status Bar * Strip Trailing Whitespace * Tabs * Tree View - * Wrap Guide - -### Package Layout + * [Wrap Guide](#wrap-guide) From 43ecc876be091aeb6ce24664f19b27adf79f7c19 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 10:25:39 -0800 Subject: [PATCH 23/83] Move package layout docs to packages/intro.md --- docs/configuring-and-extending.md | 144 ------------------------------ docs/packages/intro.md | 127 ++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 144 deletions(-) diff --git a/docs/configuring-and-extending.md b/docs/configuring-and-extending.md index b452efe04..f868cdf96 100644 --- a/docs/configuring-and-extending.md +++ b/docs/configuring-and-extending.md @@ -160,150 +160,6 @@ semantics and those of stylesheets, but they should be negligible in practice. # Packages -## Installing Packages (Partially Implemented) - -To install a package, clone it into the `~/.atom/packages` directory. -If you want to disable a package without removing it from the packages -directory, insert its name into `config.core.disabledPackages`: - -config.cson: -```coffeescript -core: - disabledPackages: [ - "fuzzy-finder", - "tree-view" - ] -``` - -## Anatomy of a Package - -A package can contain a variety of different resource types to change Atom's -behavior. The basic package layout is as follows (not every package will -have all of these directories): - -```text -my-package/ - lib/ - config/ - stylesheets/ - keymaps/ - snippets/ - grammars/ - package.json - index.coffee -``` - -**NOTE: NPM behavior is partially implemented until we get a working Node.js -API built into Atom. The goal is to make Atom packages be a superset of NPM -packages** - -### package.json - -Similar to npm packages, Atom packages can contain a `package.json` file in their -top-level directory. This file contains metadata about the package, such as the -path to its "main" module, library dependencies, and manifests specifying the -order in which its resources should be loaded. - -### Source Code - -If you want to extend Atom's behavior, your package should contain a single -top-level module, which you export from `index.coffee` or another file as -indicated by the `main` key in your `package.json` file. The remainder of your -code should be placed in the `lib` directory, and required from your top-level -file. - -Your package's top-level module is a singleton object that manages the lifecycle -of your extensions to Atom. Even if your package creates ten different views and -appends them to different parts of the DOM, it's all managed from your top-level -object. Your package's top-level module should implement the following methods: - -- `activate(rootView, state)` **Required**: This method is called when your -package is loaded. It is always passed the window's global `rootView`, and is -sometimes passed state data if the window has been reloaded and your module -implements the `serialize` method. - -- `serialize()` **Optional**: This method is called when the window is shutting -down, allowing you to return JSON to represent the state of your component. When -the window is later restored, the data you returned will be passed to your -module's `activate` method so you can restore your view to where the user left -off. - -- `deactivate()` **Optional**: This method is called when the window is shutting -down. If your package is watching any files or holding external resources in any -other way, release them here. If you're just subscribing to things on window -you don't need to worry because that's getting torn down anyway. - -#### A Simple Package Layout: - -```text -my-package/ - package.json # optional - index.coffee - lib/ - my-package.coffee -``` - -`index.coffee`: -```coffeescript -module.exports = require "./lib/my-package" -``` - -`my-package/my-package.coffee`: -```coffeescript -module.exports = - activate: (rootView, state) -> # ... - deactivate: -> # ... - serialize: -> # ... -``` - -Beyond this simple contract, your package has full access to Atom's internal -API. Anything we call internally, you can call as well. Be aware that since we -are early in development, APIs are subject to change and we have not yet -established clear boundaries between what is public and what is private. Also, -Please collaborate with us if you need an API that doesn't exist. Our goal is -to build out Atom's API organically based on the needs of package authors like -you. See [Atom's built-in packages](https://github.com/github/atom/tree/master/src/packages) -for examples of Atom's API in action. - -### Config Settings - -### Stylesheets - -### Keymaps (Not Implemented) - -Keymaps are placed in the `keymaps` subdirectory. By default, all keymaps will be -loaded in alphabetical order unless there is a `keymaps` array in `package.json` -specifying which keymaps to load and in what order. It's a good idea to provide -default keymaps for your extension. They can be customized by users later. See -the **main keymaps documentation** (todo) for more information. - -### Snippets (Not Implemented) - -An extension can supply snippets in a `snippets` directory as `.cson` or `.json` -files: - -```coffeescript -".source.coffee .specs": - "Expect": - prefix: "ex" - body: "expect($1).to$2" - "Describe": - prefix: "de" - body: """ - describe "${1:description}", -> - ${2:body} - """ -``` - -A snippets file contains scope selectors at its top level. Each scope selector -contains a hash of snippets keyed by their name. Each snippet specifies a `prefix` -and a `body` key. - -All files in the directory will be automatically loaded, unless the -`package.json` supplies a `snippets` key as a manifest. As with all scoped items, -snippets loaded later take precedence over earlier snippets when two snippets -match a scope with the same specificity. - ### Grammars ## TextMate Compatibility diff --git a/docs/packages/intro.md b/docs/packages/intro.md index 8cae71ad6..30b318803 100644 --- a/docs/packages/intro.md +++ b/docs/packages/intro.md @@ -2,6 +2,133 @@ ### Package Layout +A package can contain a variety of different resource types to change Atom's +behavior. The basic package layout is as follows (not every package will +have all of these directories): + +```text +my-package/ + lib/ + config/ + stylesheets/ + keymaps/ + snippets/ + grammars/ + package.json + index.coffee +``` + +**NOTE: NPM behavior is partially implemented until we get a working Node.js +API built into Atom. The goal is to make Atom packages be a superset of NPM +packages** + +#### package.json + +Similar to npm packages, Atom packages can contain a `package.json` file in their +top-level directory. This file contains metadata about the package, such as the +path to its "main" module, library dependencies, and manifests specifying the +order in which its resources should be loaded. + +#### Source Code + +If you want to extend Atom's behavior, your package should contain a single +top-level module, which you export from `index.coffee` or another file as +indicated by the `main` key in your `package.json` file. The remainder of your +code should be placed in the `lib` directory, and required from your top-level +file. + +Your package's top-level module is a singleton object that manages the lifecycle +of your extensions to Atom. Even if your package creates ten different views and +appends them to different parts of the DOM, it's all managed from your top-level +object. Your package's top-level module should implement the following methods: + +- `activate(rootView, state)` **Required**: This method is called when your +package is loaded. It is always passed the window's global `rootView`, and is +sometimes passed state data if the window has been reloaded and your module +implements the `serialize` method. + +- `serialize()` **Optional**: This method is called when the window is shutting +down, allowing you to return JSON to represent the state of your component. When +the window is later restored, the data you returned will be passed to your +module's `activate` method so you can restore your view to where the user left +off. + +- `deactivate()` **Optional**: This method is called when the window is shutting +down. If your package is watching any files or holding external resources in any +other way, release them here. If you're just subscribing to things on window +you don't need to worry because that's getting torn down anyway. + +#### A Simple Package Layout: + +```text +my-package/ + package.json # optional + index.coffee + lib/ + my-package.coffee +``` + +`index.coffee`: +```coffeescript +module.exports = require "./lib/my-package" +``` + +`my-package/my-package.coffee`: +```coffeescript +module.exports = + activate: (rootView, state) -> # ... + deactivate: -> # ... + serialize: -> # ... +``` + +Beyond this simple contract, your package has full access to Atom's internal +API. Anything we call internally, you can call as well. Be aware that since we +are early in development, APIs are subject to change and we have not yet +established clear boundaries between what is public and what is private. Also, +Please collaborate with us if you need an API that doesn't exist. Our goal is +to build out Atom's API organically based on the needs of package authors like +you. See [Atom's built-in packages](https://github.com/github/atom/tree/master/src/packages) +for examples of Atom's API in action. + +#### Config Settings + +#### Stylesheets + +#### Keymaps (Not Implemented) + +Keymaps are placed in the `keymaps` subdirectory. By default, all keymaps will be +loaded in alphabetical order unless there is a `keymaps` array in `package.json` +specifying which keymaps to load and in what order. It's a good idea to provide +default keymaps for your extension. They can be customized by users later. See +the **main keymaps documentation** (todo) for more information. + +#### Snippets (Not Implemented) + +An extension can supply snippets in a `snippets` directory as `.cson` or `.json` +files: + +```coffeescript +".source.coffee .specs": + "Expect": + prefix: "ex" + body: "expect($1).to$2" + "Describe": + prefix: "de" + body: """ + describe "${1:description}", -> + ${2:body} + """ +``` + +A snippets file contains scope selectors at its top level. Each scope selector +contains a hash of snippets keyed by their name. Each snippet specifies a `prefix` +and a `body` key. + +All files in the directory will be automatically loaded, unless the +`package.json` supplies a `snippets` key as a manifest. As with all scoped items, +snippets loaded later take precedence over earlier snippets when two snippets +match a scope with the same specificity. + ### Included Packages Atom comes with several built-in packages that add features to the default From 7a5d8da20159b7325156c8e32f8fb8eacc00092f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 10:33:57 -0800 Subject: [PATCH 24/83] Add installing package doc file --- .github | 2 +- docs/packages/adding.md | 1 - docs/packages/installing.md | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 docs/packages/adding.md create mode 100644 docs/packages/installing.md diff --git a/.github b/.github index 083168644..079fcb433 100644 --- a/.github +++ b/.github @@ -1,3 +1,3 @@ [docs] title = The Guide to Atom - manifest = intro.md, features.md, configuring-and-extending.md, styling.md, packages/intro.md, packages/adding.md, packages/markdown-preview.md, packages/wrap-guide.md + manifest = intro.md, features.md, configuring-and-extending.md, styling.md, packages/intro.md, packages/installing.md, packages/markdown-preview.md, packages/wrap-guide.md diff --git a/docs/packages/adding.md b/docs/packages/adding.md deleted file mode 100644 index 803454868..000000000 --- a/docs/packages/adding.md +++ /dev/null @@ -1 +0,0 @@ -## Adding a package diff --git a/docs/packages/installing.md b/docs/packages/installing.md new file mode 100644 index 000000000..38dc43160 --- /dev/null +++ b/docs/packages/installing.md @@ -0,0 +1,14 @@ +## Installing Packages (Partially Implemented) + +To install a package, clone it into the `~/.atom/packages` directory. +If you want to disable a package without removing it from the packages +directory, insert its name into `config.core.disabledPackages`: + +config.cson: +```coffeescript +core: + disabledPackages: [ + "fuzzy-finder", + "tree-view" + ] +``` From e33f93b40c6b65e951ae49aa3537040bd67fe386 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 9 Jan 2013 11:43:04 -0700 Subject: [PATCH 25/83] Load snippets from TextMate bundles There's still a bunch of holes in this. TextMate snippets have features that we don't support yet. But the basic ones should now work. --- .../snippets/spec/snippets-spec.coffee | 25 +++++++++++++++++-- .../snippets/src/package-extensions.coffee | 16 ++++++++++++ src/packages/snippets/src/snippet.coffee | 4 +-- src/packages/snippets/src/snippets.coffee | 1 - src/stdlib/fs.coffee | 8 ++++++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 29f96ad03..91a9e044d 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -5,11 +5,15 @@ Buffer = require 'buffer' Editor = require 'editor' _ = require 'underscore' fs = require 'fs' +AtomPackage = require 'atom-package' +TextMatePackage = require 'text-mate-package' describe "Snippets extension", -> [buffer, editor] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) + spyOn(AtomPackage.prototype, 'loadSnippets') + spyOn(TextMatePackage.prototype, 'loadSnippets') atom.loadPackage("snippets") editor = rootView.getActiveEditor() buffer = editor.getBuffer() @@ -31,8 +35,8 @@ describe "Snippets extension", -> "tab stops": prefix: "t2" body: """ - go here next:($2) and finally go here:($3) - go here first:($1) + go here next:($1) and finally go here:($2) + go here first:($0) """ @@ -204,8 +208,25 @@ describe "Snippets extension", -> describe "snippet loading", -> it "loads snippets from all atom packages with a snippets directory", -> + jasmine.unspy(AtomPackage.prototype, 'loadSnippets') + snippets.loadAll() + expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet + it "loads snippets from all TextMate packages with snippets", -> + jasmine.unspy(TextMatePackage.prototype, 'loadSnippets') + snippets.loadAll() + + snippet = syntax.getProperty(['.source.js'], 'snippets.fun') + expect(snippet.constructor).toBe Snippet + expect(snippet.prefix).toBe 'fun' + expect(snippet.name).toBe 'Function' + expect(snippet.body).toBe """ + function function_name (argument) { + \t// body... + } + """ + describe "Snippets parser", -> it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> bodyTree = Snippets.parser.parse """ diff --git a/src/packages/snippets/src/package-extensions.coffee b/src/packages/snippets/src/package-extensions.coffee index f4de953f7..8ce001487 100644 --- a/src/packages/snippets/src/package-extensions.coffee +++ b/src/packages/snippets/src/package-extensions.coffee @@ -9,3 +9,19 @@ AtomPackage.prototype.loadSnippets = -> snippets.load(snippetsPath) TextMatePackage.prototype.loadSnippets = -> + snippetsDirPath = fs.join(@path, 'Snippets') + if fs.exists(snippetsDirPath) + tmSnippets = fs.list(snippetsDirPath).map (snippetPath) -> fs.readPlist(snippetPath) + snippets.add(@translateSnippets(tmSnippets)) + +TextMatePackage.prototype.translateSnippets = (tmSnippets) -> + atomSnippets = {} + for { scope, name, content, tabTrigger } in tmSnippets + if scope + scope = TextMatePackage.cssSelectorFromScopeSelector(scope) + else + scope = '*' + + snippetsForScope = (atomSnippets[scope] ?= {}) + snippetsForScope[name] = { prefix: tabTrigger, body: content } + atomSnippets diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index b55923d38..51e2e7c5f 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -20,12 +20,12 @@ class Snippet # recursive helper function; mutates vars above extractTabStops = (bodyTree) -> for segment in bodyTree - if segment.index + if segment.index? { index, content } = segment start = [row, column] extractTabStops(content) tabStopsByIndex[index] = new Range(start, [row, column]) - else + else if _.isString(segment) bodyText.push(segment) segmentLines = segment.split('\n') column += segmentLines.shift().length diff --git a/src/packages/snippets/src/snippets.coffee b/src/packages/snippets/src/snippets.coffee index 5ff21f0b7..140c04eca 100644 --- a/src/packages/snippets/src/snippets.coffee +++ b/src/packages/snippets/src/snippets.coffee @@ -35,7 +35,6 @@ module.exports = snippetsByPrefix[snippet.prefix] = snippet syntax.addProperties(selector, snippets: snippetsByPrefix) - enableSnippetsInEditor: (editor) -> editor.command 'snippets:expand', (e) => editSession = editor.activeEditSession diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index 4d2bd1760..53d3e4b1f 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -184,3 +184,11 @@ module.exports = CoffeeScript.eval(contents, bare: true) else JSON.parse(contents) + + readPlist: (path) -> + plist = require 'plist' + object = null + plist.parseString @read(path), (e, data) -> + throw new Error(e) if e + object = data[0] + object From c56788fd04c212f6c601e0e70bbdaf9f961eab41 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 9 Jan 2013 09:20:19 -0800 Subject: [PATCH 26/83] Catch packing loading exceptions in Package@load Instead of in AtomPackage.load --- spec/app/atom-spec.coffee | 5 +++++ .../package-that-throws-an-exception/index.coffee | 1 + src/app/atom-package.coffee | 11 ++++------- src/app/package.coffee | 11 +++++++---- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 spec/fixtures/packages/package-that-throws-an-exception/index.coffee diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 58fa6f810..a117d48b8 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -18,6 +18,11 @@ describe "the `atom` global", -> atom.loadPackage("package-with-module") expect(rootView.activatePackage).toHaveBeenCalledWith('package-with-module', extension) + it "logs warning instead of throwing an exception if a package fails to load", -> + spyOn(console, "warn") + expect(-> atom.loadPackage("package-that-throws-an-exception")).not.toThrow() + expect(console.warn).toHaveBeenCalled() + describe "keymap loading", -> describe "when package.json does not contain a 'keymaps' manifest", -> it "loads all keymaps in the directory", -> diff --git a/spec/fixtures/packages/package-that-throws-an-exception/index.coffee b/spec/fixtures/packages/package-that-throws-an-exception/index.coffee new file mode 100644 index 000000000..9e2c06779 --- /dev/null +++ b/spec/fixtures/packages/package-that-throws-an-exception/index.coffee @@ -0,0 +1 @@ +throw new Error("This package throws an exception") \ No newline at end of file diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index bb91b60af..698c10998 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -14,13 +14,10 @@ class AtomPackage extends Package @module.name = @name load: -> - try - @loadMetadata() - @loadKeymaps() - @loadStylesheets() - rootView.activatePackage(@name, @module) if @module - catch e - console.error "Failed to load package named '#{@name}'", e.stack + @loadMetadata() + @loadKeymaps() + @loadStylesheets() + rootView.activatePackage(@name, @module) if @module loadMetadata: -> if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json']) diff --git a/src/app/package.coffee b/src/app/package.coffee index 49d6445ac..76f778f69 100644 --- a/src/app/package.coffee +++ b/src/app/package.coffee @@ -6,10 +6,13 @@ class Package AtomPackage = require 'atom-package' TextMatePackage = require 'text-mate-package' - if TextMatePackage.testName(name) - new TextMatePackage(name).load() - else - new AtomPackage(name).load() + try + if TextMatePackage.testName(name) + new TextMatePackage(name).load() + else + new AtomPackage(name).load() + catch e + console.warn "Failed to load package named '#{name}'", e.stack name: null From d4b74f9858b7f38ea6dc41b3e41f47189792c12b Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 9 Jan 2013 10:43:04 -0800 Subject: [PATCH 27/83] Migrate to new RootView serialization scheme --- src/app/root-view.coffee | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 5adee2902..3b14e4378 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -23,9 +23,13 @@ class RootView extends View @div id: 'vertical', outlet: 'vertical', => @div id: 'panes', outlet: 'panes' - @deserialize: ({ projectState, panesViewState, packageStates }) -> - project = Project.deserialize(projectState) if projectState - rootView = new RootView(project, packageStates: packageStates, suppressOpen: true) + @deserialize: ({ projectState, panesViewState, packageStates, projectPath }) -> + if projectState + projectOrPathToOpen = Project.deserialize(projectState) + else + projectOrPathToOpen = projectPath # This will migrate people over to the new project serialization scheme. It should be removed eventually. + + rootView = new RootView(projectOrPathToOpen , packageStates: packageStates, suppressOpen: true) rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState rootView @@ -38,7 +42,7 @@ class RootView extends View @packageStates ?= {} @packageModules = {} - if not projectOrPathToOpen or _.isString(projectOrPathToOpen) + if not projectOrPathToOpen or _.isString(projectOrPathToOpen) pathToOpen = projectOrPathToOpen @project = new Project(projectOrPathToOpen) else From 3e7f710b3549c8999b1dd730cdc4068c98a6f3c9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 11:07:45 -0800 Subject: [PATCH 28/83] :lipstick: --- src/app/root-view.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 3b14e4378..9184d49f1 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -25,10 +25,10 @@ class RootView extends View @deserialize: ({ projectState, panesViewState, packageStates, projectPath }) -> if projectState - projectOrPathToOpen = Project.deserialize(projectState) + projectOrPathToOpen = Project.deserialize(projectState) else projectOrPathToOpen = projectPath # This will migrate people over to the new project serialization scheme. It should be removed eventually. - + rootView = new RootView(projectOrPathToOpen , packageStates: packageStates, suppressOpen: true) rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState rootView @@ -42,7 +42,7 @@ class RootView extends View @packageStates ?= {} @packageModules = {} - if not projectOrPathToOpen or _.isString(projectOrPathToOpen) + if not projectOrPathToOpen or _.isString(projectOrPathToOpen) pathToOpen = projectOrPathToOpen @project = new Project(projectOrPathToOpen) else From 6c2607a5e056325758c18c331eb7b07ec4ebfdcd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 9 Jan 2013 12:32:11 -0700 Subject: [PATCH 29/83] Sort `$0` tab stops last instead of first for TextMate compatibility --- src/packages/snippets/spec/snippets-spec.coffee | 4 ++-- src/packages/snippets/src/snippet.coffee | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 91a9e044d..fd8cea73d 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -35,8 +35,8 @@ describe "Snippets extension", -> "tab stops": prefix: "t2" body: """ - go here next:($1) and finally go here:($2) - go here first:($0) + go here next:($2) and finally go here:($0) + go here first:($1) """ diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index 51e2e7c5f..ef7c3d0aa 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -22,6 +22,7 @@ class Snippet for segment in bodyTree if segment.index? { index, content } = segment + index = Infinity if index == 0 start = [row, column] extractTabStops(content) tabStopsByIndex[index] = new Range(start, [row, column]) From ba614d5549b495132b4e8a1a18929a413b557080 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 11:51:31 -0800 Subject: [PATCH 30/83] Throw error from GitRepository constructor --- native/v8_extensions/git.js | 11 ++++++----- spec/app/git-spec.coffee | 2 +- src/app/git.coffee | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/native/v8_extensions/git.js b/native/v8_extensions/git.js index 4354c9f61..aaae57510 100644 --- a/native/v8_extensions/git.js +++ b/native/v8_extensions/git.js @@ -13,11 +13,12 @@ var $git = {}; function GitRepository(path) { var repo = getRepository(path); - if (repo) { - repo.constructor = GitRepository; - repo.__proto__ = GitRepository.prototype; - return repo; - } + if (!repo) + throw new Error("No Git repository found searching path: " + path); + + repo.constructor = GitRepository; + repo.__proto__ = GitRepository.prototype; + return repo; } GitRepository.prototype.getHead = getHead; diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index ace54d1c4..36df69cdd 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -8,7 +8,7 @@ describe "Git", -> describe "@open(path)", -> it "returns null when no repository is found", -> - expect(Git.open('/tmp/nogit.txt')).toBeNull(0) + expect(Git.open('/tmp/nogit.txt')).toBeNull() describe "new Git(path)", -> it "throws an exception when no repository is found", -> diff --git a/src/app/git.coffee b/src/app/git.coffee index cdca748b0..ac8e6e3e0 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -23,8 +23,6 @@ class Git constructor: (path) -> @repo = new GitRepository(path) - unless @repo? - throw new Error("No Git repository found searching path: #{path}") $(window).on 'focus', => @refreshIndex() refreshIndex: -> @repo.refreshIndex() From d5a23f770fa1245b86f2ac062b27d85834c5c9b0 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 9 Jan 2013 11:38:41 -0800 Subject: [PATCH 31/83] Override meta-w to be a noop on tool-panels --- src/app/keymaps/atom.cson | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 76cc697c6..0780ad30e 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -33,3 +33,4 @@ '.tool-panel': 'meta-escape': 'tool-panel:unfocus' 'escape': 'core:close' + 'meta-w': 'noop' \ No newline at end of file From 261a8aae2d6c20ac3d0c036618d885fa402dc454 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 9 Jan 2013 15:22:37 -0800 Subject: [PATCH 32/83] Remove @autoIndent from Project and EditSession --- src/app/edit-session.coffee | 3 +-- src/app/editor.coffee | 1 - src/app/project.coffee | 5 ----- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 04aa8d523..2eb241a89 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -33,11 +33,10 @@ class EditSession anchorRanges: null cursors: null selections: null - autoIndent: false # TODO: re-enabled auto-indent after fixing the rest of tokenization softTabs: true softWrap: false - constructor: ({@project, @buffer, tabLength, @autoIndent, softTabs, @softWrap }) -> + constructor: ({@project, @buffer, tabLength, softTabs, @softWrap }) -> @softTabs = @buffer.usesSoftTabs() ? softTabs ? true @languageMode = new LanguageMode(this, @buffer.getExtension()) @displayBuffer = new DisplayBuffer(@buffer, { @languageMode, tabLength }) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 5a67ef28e..651811574 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -80,7 +80,6 @@ class Editor extends View buffer: new Buffer() softWrap: false tabLength: 2 - autoIndent: false softTabs: true @editSessions.push editSession diff --git a/src/app/project.coffee b/src/app/project.coffee index d49a1dae9..82d2f18dd 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -16,7 +16,6 @@ class Project new Project(state.path, state.grammarOverridesByPath) tabLength: 2 - autoIndent: true softTabs: true softWrap: false rootDirectory: null @@ -91,9 +90,6 @@ class Project relativize: (fullPath) -> fullPath.replace(@getPath(), "").replace(/^\//, '') - getAutoIndent: -> @autoIndent - setAutoIndent: (@autoIndent) -> - getSoftTabs: -> @softTabs setSoftTabs: (@softTabs) -> @@ -114,7 +110,6 @@ class Project defaultEditSessionOptions: -> tabLength: @tabLength - autoIndent: @getAutoIndent() softTabs: @getSoftTabs() softWrap: @getSoftWrap() From f5ee676e5e25a62c9128d276a37db097ae21799d Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 9 Jan 2013 15:24:04 -0800 Subject: [PATCH 33/83] Pass autoIndent as an option flag Instead of querying EditSession for autoIndenting --- spec/app/edit-session-spec.coffee | 56 +++++++++++-------------------- src/app/edit-session.coffee | 22 ++++++++---- src/app/editor.coffee | 4 +-- src/app/selection.coffee | 8 ++--- 4 files changed, 41 insertions(+), 49 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 4b70927d9..f82d7b263 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -675,10 +675,7 @@ describe "EditSession", -> editSession.insertText('holy cow') expect(editSession.lineForScreenRow(2).fold).toBeUndefined() - describe "when auto-indent is enabled and the `autoIndent` option is true", -> - beforeEach -> - editSession.setAutoIndent(true) - + describe "when auto-indent is enabled", -> describe "when a single newline is inserted", -> describe "when the newline is inserted on a line that starts a new level of indentation", -> it "auto-indents the new line to one additional level of indentation beyond the preceding line", -> @@ -739,16 +736,13 @@ describe "EditSession", -> removeLeadingWhitespace = (text) -> text.replace(/^\s*/, '') describe "when the cursor is preceded only by whitespace", -> - describe "when auto-indent is enabled", -> - beforeEach -> - editSession.setAutoIndent(true) - + describe "when auto-indent is enabled", -> describe "when the cursor's current column is less than the suggested indent level", -> describe "when the indentBasis is inferred from the first line", -> it "indents all lines relative to the suggested indent", -> - editSession.insertText('\n xx') + editSession.insertText('\n xx', autoIndent: true) editSession.setCursorBufferPosition([3, 1]) - editSession.insertText(text, normalizeIndent: true) + editSession.insertText(text, normalizeIndent: true, autoIndent: true) expect(editSession.lineForBufferRow(3)).toBe " while (true) {" expect(editSession.lineForBufferRow(4)).toBe " foo();" @@ -759,7 +753,7 @@ describe "EditSession", -> it "indents all lines relative to the suggested indent", -> editSession.insertText('\n xx') editSession.setCursorBufferPosition([3, 1]) - editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2) + editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2, autoIndent: true) expect(editSession.lineForBufferRow(3)).toBe " while (true) {" expect(editSession.lineForBufferRow(4)).toBe " foo();" @@ -775,7 +769,7 @@ describe "EditSession", -> """ editSession.setCursorBufferPosition([1, 0]) - editSession.insertText(text, normalizeIndent: true) + editSession.insertText(text, normalizeIndent: true, autoIndent: true) expect(editSession.lineForBufferRow(1)).toBe "\t\t\twhile (true) {" expect(editSession.lineForBufferRow(2)).toBe "\t\t\t\tfoo();" @@ -791,7 +785,7 @@ describe "EditSession", -> """ editSession.setCursorBufferPosition([1, 0]) - editSession.insertText(text, normalizeIndent: true) + editSession.insertText(text, normalizeIndent: true, autoIndent: true) expect(editSession.lineForBufferRow(1)).toBe "\t\twhile (true) {" expect(editSession.lineForBufferRow(2)).toBe "\t\t\tfoo();" @@ -820,9 +814,6 @@ describe "EditSession", -> expect(editSession.lineForBufferRow(6)).toBe " bar();" describe "if auto-indent is disabled", -> - beforeEach -> - expect(editSession.autoIndent).toBeFalsy() - describe "when the indentBasis is inferred from the first line", -> it "always normalizes indented lines to the cursor's current indentation level", -> editSession.insertText('\n ') @@ -845,7 +836,6 @@ describe "EditSession", -> describe "when the cursor is preceded by non-whitespace characters", -> describe "when the indentBasis is inferred from the first line", -> it "normalizes the indentation level of all lines based on the level of the existing first line", -> - editSession.setAutoIndent(true) editSession.buffer.delete([[2, 0], [2, 2]]) editSession.insertText(text, normalizeIndent:true) @@ -856,7 +846,6 @@ describe "EditSession", -> describe "when an indentBasis is provided", -> it "normalizes the indentation level of all lines based on the level of the existing first line", -> - editSession.setAutoIndent(true) editSession.buffer.delete([[2, 0], [2, 2]]) editSession.insertText(removeLeadingWhitespace(text), normalizeIndent:true, indentBasis: 2) @@ -1311,8 +1300,7 @@ describe "EditSession", -> it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", -> buffer.insert([5, 0], " \n") editSession.setCursorBufferPosition [5, 0] - editSession.setAutoIndent(true) - editSession.indent() + editSession.indent(autoIndent: true) expect(buffer.lineForRow(5)).toMatch /^\s+$/ expect(buffer.lineForRow(5).length).toBe 6 expect(editSession.getCursorBufferPosition()).toEqual [5, 6] @@ -1323,8 +1311,7 @@ describe "EditSession", -> editSession.softTabs = false buffer.insert([5, 0], "\t\n") editSession.setCursorBufferPosition [5, 0] - editSession.setAutoIndent(true) - editSession.indent() + editSession.indent(autoIndent: true) expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ expect(editSession.getCursorBufferPosition()).toEqual [5, 3] @@ -1333,8 +1320,7 @@ describe "EditSession", -> it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> buffer.insert([7, 0], " \n") editSession.setCursorBufferPosition [7, 2] - editSession.setAutoIndent(true) - editSession.indent() + editSession.indent(autoIndent: true) expect(buffer.lineForRow(7)).toMatch /^\s+$/ expect(buffer.lineForRow(7).length).toBe 8 expect(editSession.getCursorBufferPosition()).toEqual [7, 8] @@ -1345,8 +1331,7 @@ describe "EditSession", -> editSession.softTabs = false buffer.insert([7, 0], "\t\t\t\n") editSession.setCursorBufferPosition [7, 1] - editSession.setAutoIndent(true) - editSession.indent() + editSession.indent(autoIndent: true) expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ expect(editSession.getCursorBufferPosition()).toEqual [7, 4] @@ -1421,17 +1406,16 @@ describe "EditSession", -> expect(editSession.buffer.lineForRow(0)).toBe "var first = function () {" expect(buffer.lineForRow(1)).toBe " var first = function(items) {" - it "preserves the indent level when copying and pasting multiple lines", -> - editSession.setAutoIndent(true) - editSession.setSelectedBufferRange([[4, 4], [7, 5]]) - editSession.copySelectedText() - editSession.setCursorBufferPosition([10, 0]) - editSession.pasteText() + it "preserves the indent level when copying and pasting multiple lines", -> + editSession.setSelectedBufferRange([[4, 4], [7, 5]]) + editSession.copySelectedText() + editSession.setCursorBufferPosition([10, 0]) + editSession.pasteText(autoIndent: true) - expect(editSession.lineForBufferRow(10)).toBe " while(items.length > 0) {" - expect(editSession.lineForBufferRow(11)).toBe " current = items.shift();" - expect(editSession.lineForBufferRow(12)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editSession.lineForBufferRow(13)).toBe " }" + expect(editSession.lineForBufferRow(10)).toBe " while(items.length > 0) {" + expect(editSession.lineForBufferRow(11)).toBe " current = items.shift();" + expect(editSession.lineForBufferRow(12)).toBe " current < pivot ? left.push(current) : right.push(current);" + expect(editSession.lineForBufferRow(13)).toBe " }" describe ".indentSelectedRows()", -> describe "when nothing is selected", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 2eb241a89..761c6dba8 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -92,7 +92,6 @@ class EditSession getScrollLeft: -> @scrollLeft setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn) - setAutoIndent: (@autoIndent) -> setSoftTabs: (@softTabs) -> getSoftWrap: -> @softWrap @@ -158,18 +157,23 @@ class EditSession getCursorScopes: -> @getCursor().getScopes() logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) - insertText: (text, options) -> + shouldAutoIndent: -> + false + + insertText: (text, options={}) -> + options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.insertText(text, options) insertNewline: -> - @insertText('\n', autoIndent: true) + @insertText('\n') insertNewlineBelow: -> @moveCursorToEndOfLine() @insertNewline() - indent: -> - @mutateSelectedText (selection) -> selection.indent() + indent: (options={})-> + options.autoIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) -> selection.indent(options) backspace: -> @mutateSelectedText (selection) -> selection.backspace() @@ -216,9 +220,13 @@ class EditSession selection.copy(maintainPasteboard) maintainPasteboard = true - pasteText: -> + pasteText: (options={})-> + options.normalizeIndent ?= true + [text, metadata] = pasteboard.read() - @insertText(text, _.extend(metadata ? {}, normalizeIndent: true)) + _.extend(options, metadata) if metadata + + @insertText(text, options) undo: -> @buffer.undo(this) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 651811574..40b64d64d 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -243,7 +243,7 @@ class Editor extends View insertText: (text, options) -> @activeEditSession.insertText(text, options) insertNewline: -> @activeEditSession.insertNewline() insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() - indent: -> @activeEditSession.indent() + indent: (options) -> @activeEditSession.indent(options) indentSelectedRows: -> @activeEditSession.indentSelectedRows() outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() cutSelection: -> @activeEditSession.cutSelectedText() @@ -380,7 +380,7 @@ class Editor extends View @selectOnMousemoveUntilMouseup() @on "textInput", (e) => - @insertText(e.originalEvent.data, autoIndent: true) + @insertText(e.originalEvent.data) false @scrollView.on 'mousewheel', (e) => diff --git a/src/app/selection.coffee b/src/app/selection.coffee index e7f7c23db..dce6f1b3e 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -179,13 +179,13 @@ class Selection else @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed - if @editSession.autoIndent and options.autoIndent + if options.autoIndent if text == '\n' @editSession.autoIndentBufferRow(newBufferRange.end.row) else @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) - indent: -> + indent: ({ autoIndent }={})-> { row, column } = @cursor.getBufferPosition() if @isEmpty() @@ -193,7 +193,7 @@ class Selection desiredIndent = @editSession.suggestedIndentForBufferRow(row) delta = desiredIndent - @cursor.getIndentLevel() - if @editSession.autoIndent and delta > 0 + if autoIndent and delta > 0 @insertText(@editSession.buildIndentString(delta)) else @insertText(@editSession.getTabText()) @@ -221,7 +221,7 @@ class Selection if insideExistingLine desiredBasis = @editSession.indentationForBufferRow(currentBufferRow) - else if @editSession.autoIndent + else if options.autoIndent desiredBasis = @editSession.suggestedIndentForBufferRow(currentBufferRow) else desiredBasis = @cursor.getIndentLevel() From 21fa3e5a0f54c93218a94d5befaf7e41581bc563 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 9 Jan 2013 16:26:57 -0800 Subject: [PATCH 34/83] autoIndent is stored as a syntax property --- spec/app/edit-session-spec.coffee | 15 +++++++++++++++ src/app/edit-session.coffee | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index f82d7b263..9dd4375ab 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1915,3 +1915,18 @@ describe "EditSession", -> editSession.setCursorScreenPosition([0, 1]) editSession.buffer.reload() expect(editSession.getCursorScreenPosition()).toEqual [0,1] + + describe "autoIndent", -> + describe "when editor.autoIndent returns true based on the EditSession's grammar scope", -> + it "auto indents lines", -> + syntax.addProperties("." + editSession.languageMode.grammar.scopeName, editor: autoIndent: true ) + editSession.setCursorBufferPosition([1, 30]) + editSession.insertText("\n") + expect(editSession.lineForBufferRow(2)).toBe " " + + describe "when editor.autoIndent returns false based on the EditSession's grammar scope", -> + it "auto indents lines", -> + syntax.addProperties("." + editSession.languageMode.grammar.scopeName, editor: autoIndent: false ) + editSession.setCursorBufferPosition([1, 30]) + editSession.insertText("\n") + expect(editSession.lineForBufferRow(2)).toBe "" diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 761c6dba8..32c751966 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -158,7 +158,7 @@ class EditSession logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) shouldAutoIndent: -> - false + syntax.getProperty(["." + @languageMode.grammar.scopeName], "editor.autoIndent") ? false insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() From caffda602727bf36be07a41c7139c222442ce2e5 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Wed, 9 Jan 2013 17:07:29 -0800 Subject: [PATCH 35/83] Add Git.destroy() that frees native repository --- native/v8_extensions/git.js | 2 ++ native/v8_extensions/git.mm | 19 ++++++++++++++++--- src/app/git.coffee | 11 ++++++++++- src/app/project.coffee | 2 ++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/native/v8_extensions/git.js b/native/v8_extensions/git.js index aaae57510..fa4b1b49c 100644 --- a/native/v8_extensions/git.js +++ b/native/v8_extensions/git.js @@ -10,6 +10,7 @@ var $git = {}; native function getDiffStats(path); native function isSubmodule(path); native function refreshIndex(); + native function destroy(); function GitRepository(path) { var repo = getRepository(path); @@ -29,5 +30,6 @@ var $git = {}; GitRepository.prototype.getDiffStats = getDiffStats; GitRepository.prototype.isSubmodule = isSubmodule; GitRepository.prototype.refreshIndex = refreshIndex; + GitRepository.prototype.destroy = destroy; this.GitRepository = GitRepository; })(); diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 39ef3dc0d..b0d8cebe9 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -16,10 +16,17 @@ public: } ~GitRepository() { - git_repository_free(repo); + Destroy(); } - BOOL exists() { + void Destroy() { + if (Exists()) { + git_repository_free(repo); + repo = NULL; + } + } + + BOOL Exists() { return repo != NULL; } @@ -190,7 +197,7 @@ bool Git::Execute(const CefString& name, CefString& exception) { if (name == "getRepository") { GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str()); - if (repository->exists()) { + if (repository->Exists()) { CefRefPtr userData = repository; retval = CefV8Value::CreateObject(NULL); retval->SetUserData(userData); @@ -248,6 +255,12 @@ bool Git::Execute(const CefString& name, return true; } + if (name == "destroy") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + userData->Destroy(); + return true; + } + return false; } diff --git a/src/app/git.coffee b/src/app/git.coffee index ac8e6e3e0..e2142385f 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -1,4 +1,6 @@ $ = require 'jquery' +_ = require 'underscore' +Subscriber = require 'subscriber' module.exports = class Git @@ -23,12 +25,17 @@ class Git constructor: (path) -> @repo = new GitRepository(path) - $(window).on 'focus', => @refreshIndex() + @subscribe $(window), 'focus', => @refreshIndex() refreshIndex: -> @repo.refreshIndex() getPath: -> @repo.getPath() + destroy: -> + @repo.destroy() + @repo = null + @unsubscribe() + getWorkingDirectory: -> repoPath = @getPath() repoPath?.substring(0, repoPath.length - 6) @@ -85,3 +92,5 @@ class Git isSubmodule: (path) -> @repo.isSubmodule(@relativize(path)) + +_.extend Git.prototype, Subscriber diff --git a/src/app/project.coffee b/src/app/project.coffee index d49a1dae9..074035fda 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -34,6 +34,8 @@ class Project grammarOverridesByPath: @grammarOverridesByPath destroy: -> + @repo?.destroy() + @repo = null editSession.destroy() for editSession in @getEditSessions() addGrammarOverrideForPath: (path, grammar) -> From bb6bed85c6800b2cd0aa827282d1445c08a6c601 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Wed, 9 Jan 2013 17:12:15 -0800 Subject: [PATCH 36/83] Raise exception when destroyed repo is accessed --- spec/app/git-spec.coffee | 6 ++++++ src/app/git.coffee | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 36df69cdd..20edab6bb 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -121,3 +121,9 @@ describe "Git", -> expect(repo.checkoutHead(path1)).toBeTruthy() expect(fs.read(path2)).toBe('path 2 is edited') expect(repo.isPathModified(path2)).toBeTruthy() + + describe ".destroy()", -> + it "throws an exception when any method is called after it is called", -> + repo = new Git(require.resolve('fixtures/git/master.git/HEAD')) + repo.destroy() + expect(-> repo.getHead()).toThrow() diff --git a/src/app/git.coffee b/src/app/git.coffee index e2142385f..a08595ffb 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -27,12 +27,17 @@ class Git @repo = new GitRepository(path) @subscribe $(window), 'focus', => @refreshIndex() - refreshIndex: -> @repo.refreshIndex() + getRepo: -> + unless @repo? + throw new Error("Repository has been destroyed") + @repo - getPath: -> @repo.getPath() + refreshIndex: -> @getRepo().refreshIndex() + + getPath: -> @getRepo().getPath() destroy: -> - @repo.destroy() + @getRepo().destroy() @repo = null @unsubscribe() @@ -41,13 +46,13 @@ class Git repoPath?.substring(0, repoPath.length - 6) getHead: -> - @repo.getHead() or '' + @getRepo().getHead() or '' getPathStatus: (path) -> - pathStatus = @repo.getStatus(@relativize(path)) + pathStatus = @getRepo().getStatus(@relativize(path)) isPathIgnored: (path) -> - @repo.isIgnored(@relativize(path)) + @getRepo().isIgnored(@relativize(path)) isStatusModified: (status) -> modifiedFlags = @statusFlags.working_dir_modified | @@ -85,12 +90,12 @@ class Git return head checkoutHead: (path) -> - @repo.checkoutHead(@relativize(path)) + @getRepo().checkoutHead(@relativize(path)) getDiffStats: (path) -> - @repo.getDiffStats(@relativize(path)) or added: 0, deleted: 0 + @getRepo().getDiffStats(@relativize(path)) or added: 0, deleted: 0 isSubmodule: (path) -> - @repo.isSubmodule(@relativize(path)) + @getRepo().isSubmodule(@relativize(path)) _.extend Git.prototype, Subscriber From 3db7af1edf15ff9b7d97e0e7c40f34b476738b3d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Wed, 9 Jan 2013 19:24:47 -0700 Subject: [PATCH 37/83] Don't show the tree view until the project has a path --- src/app/root-view.coffee | 2 +- .../tree-view/spec/tree-view-spec.coffee | 32 ++++++++++++------- src/packages/tree-view/src/tree-view.coffee | 11 +++++-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index c62bc7c90..ed0a88124 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -82,8 +82,8 @@ class RootView extends View @handleFocus(e) if document.activeElement is document.body @on 'root-view:active-path-changed', (e, path) => - @project.setPath(path) unless @project.getRootDirectory() if path + @project.setPath(path) unless @project.getRootDirectory() @setTitle(fs.base(path)) else @setTitle("untitled") diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 664022285..2606ba92f 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -14,7 +14,7 @@ describe "TreeView", -> project = rootView.project atom.loadPackage("tree-view") - treeView = rootView.find(".tree-view").view() + treeView = TreeView.instance treeView.root = treeView.find('> li:first').view() sampleJs = treeView.find('.file:contains(tree-view.js)') sampleTxt = treeView.find('.file:contains(tree-view.txt)') @@ -52,24 +52,34 @@ describe "TreeView", -> rootView = new RootView atom.loadPackage 'tree-view' - treeView = rootView.find(".tree-view").view() + treeView = TreeView.instance - it "does not create a root node", -> + it "does not attach or create a root node", -> + expect(treeView.hasParent()).toBeFalsy() expect(treeView.root).not.toExist() it "serializes without throwing an exception", -> expect(-> treeView.serialize()).not.toThrow() - it "creates a root view when the project path is created", -> - rootView.open(require.resolve('fixtures/sample.js')) - expect(treeView.root.getPath()).toBe require.resolve('fixtures') - expect(treeView.root.parent()).toMatchSelector(".tree-view") + describe "when the project is assigned a path because a buffer is opened", -> + it "attaches the tree and creates a root directory view", -> + rootView.open(require.resolve('fixtures/sample.js')) + expect(treeView.hasParent()).toBeTruthy() + expect(treeView.root.getPath()).toBe require.resolve('fixtures') + expect(treeView.root.parent()).toMatchSelector(".tree-view") - oldRoot = treeView.root + oldRoot = treeView.root - rootView.project.setPath('/tmp') - expect(treeView.root).not.toEqual oldRoot - expect(oldRoot.hasParent()).toBeFalsy() + rootView.project.setPath('/tmp') + expect(treeView.root).not.toEqual oldRoot + expect(oldRoot.hasParent()).toBeFalsy() + + describe "when the project is assigned a path because a new buffer is saved", -> + it "attaches the tree and creates a root directory view", -> + rootView.getActiveEditSession().saveAs("/tmp/test.txt") + expect(treeView.hasParent()).toBeTruthy() + expect(treeView.root.getPath()).toBe require.resolve('/tmp') + expect(treeView.root.parent()).toMatchSelector(".tree-view") describe "serialization", -> [newRootView, newTreeView] = [] diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index 7f8470b7c..c3c57bfc2 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -16,7 +16,12 @@ class TreeView extends ScrollView @instance = TreeView.deserialize(state, rootView) else @instance = new TreeView(rootView) - @instance.attach() + + if rootView.project.getPath() + @instance.attach() + else + rootView.project.one "path-changed", => + @instance.attach() @deactivate: -> @instance.deactivate() @@ -118,8 +123,8 @@ class TreeView extends ScrollView updateRoot: -> @root?.remove() - if @rootView.project.getRootDirectory() - @root = new DirectoryView(directory: @rootView.project.getRootDirectory(), isExpanded: true, project: @rootView.project) + if rootDirectory = @rootView.project.getRootDirectory() + @root = new DirectoryView(directory: rootDirectory, isExpanded: true, project: @rootView.project) @append(@root) else @root = null From 06e39595ba34f8bb311399d126b182b9441a579d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Wed, 9 Jan 2013 19:27:02 -0700 Subject: [PATCH 38/83] Make `RootView` listen for events before loading packages This is because RootView listens to some of its own events. It needs to be first in line to handle its own events because package event handlers might rely on tree view's event handlers having been run. This also brings behavior more in line with what we'll experience in specs. --- src/app/root-view.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index ed0a88124..c68249c01 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -41,6 +41,7 @@ class RootView extends View window.rootView = this @packageStates ?= {} @packageModules = {} + @handleEvents() if not projectOrPathToOpen or _.isString(projectOrPathToOpen) pathToOpen = projectOrPathToOpen @@ -50,8 +51,6 @@ class RootView extends View config.load() - @handleEvents() - if pathToOpen @open(pathToOpen) if fs.isFile(pathToOpen) and not suppressOpen else From f76db1f9571bf74ff90ef8451775caf1ac1e49cf Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 9 Jan 2013 19:21:04 -0800 Subject: [PATCH 39/83] Log 10 longest running specs --- vendor/jasmine-helper.coffee | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/vendor/jasmine-helper.coffee b/vendor/jasmine-helper.coffee index c99b44051..1c735ef8f 100644 --- a/vendor/jasmine-helper.coffee +++ b/vendor/jasmine-helper.coffee @@ -6,6 +6,7 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> nakedLoad 'jasmine-focused' $ = require 'jquery' + _ = require 'underscore' $('body').append $$ -> @div id: 'jasmine-content' @@ -18,5 +19,35 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> require specSuite jasmineEnv = jasmine.getEnv() jasmineEnv.addReporter(reporter) + + class TimeReporter extends jasmine.Reporter + + timedSpecs: [] + + reportSpecStarting: (spec) -> + stack = [spec.description] + suite = spec.suite + while suite + stack.unshift suite.description + suite = suite.parentSuite + + @time = new Date().getTime() + @description = stack.join(' -> ') + + reportSpecResults: -> + return unless @time? and @description? + @timedSpecs.push + description: @description + time: new Date().getTime() - @time + @time = null + @description = null + + reportRunnerResults: -> + console.log '10 longest running specs:' + for spec in _.sortBy(@timedSpecs, (spec) -> -spec.time)[0...10] + console.log "#{spec.time}ms" + console.log spec.description + + jasmineEnv.addReporter(new TimeReporter()) jasmineEnv.specFilter = (spec) -> reporter.specFilter(spec) jasmineEnv.execute() From 7a89de077ba706ecfbb3c651e242749738088df1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 08:36:43 -0800 Subject: [PATCH 40/83] Log longest specs explicitly Instead of logging them to the console at the end of the run, add two new methods, logLongestSpec() and logLongestSpecs(number) to the window object that will print out the results. --- spec/time-reporter.coffee | 34 ++++++++++++++++++++++++++++++++++ vendor/jasmine-helper.coffee | 30 +----------------------------- 2 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 spec/time-reporter.coffee diff --git a/spec/time-reporter.coffee b/spec/time-reporter.coffee new file mode 100644 index 000000000..c2d12870c --- /dev/null +++ b/spec/time-reporter.coffee @@ -0,0 +1,34 @@ +_ = require 'underscore' + +module.exports = +class TimeReporter extends jasmine.Reporter + + timedSpecs: [] + + constructor: -> + window.logLongestSpec = -> window.logLongestSpecs(1) + window.logLongestSpecs = (number=10) => + console.log "#{number} longest running specs:" + for spec in _.sortBy(@timedSpecs, (spec) -> -spec.time)[0...number] + console.log "#{spec.time}ms" + console.log spec.description + + reportSpecStarting: (spec) -> + stack = [spec.description] + suite = spec.suite + while suite + stack.unshift suite.description + suite = suite.parentSuite + + @time = new Date().getTime() + reducer = (memo, description, index) -> + "#{memo}#{_.multiplyString(' ', index)}#{description}\n" + @description = _.reduce(stack, reducer, "") + + reportSpecResults: -> + return unless @time? and @description? + @timedSpecs.push + description: @description + time: new Date().getTime() - @time + @time = null + @description = null diff --git a/vendor/jasmine-helper.coffee b/vendor/jasmine-helper.coffee index 1c735ef8f..0defd1990 100644 --- a/vendor/jasmine-helper.coffee +++ b/vendor/jasmine-helper.coffee @@ -6,7 +6,7 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> nakedLoad 'jasmine-focused' $ = require 'jquery' - _ = require 'underscore' + TimeReporter = require 'time-reporter' $('body').append $$ -> @div id: 'jasmine-content' @@ -20,34 +20,6 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> jasmineEnv = jasmine.getEnv() jasmineEnv.addReporter(reporter) - class TimeReporter extends jasmine.Reporter - - timedSpecs: [] - - reportSpecStarting: (spec) -> - stack = [spec.description] - suite = spec.suite - while suite - stack.unshift suite.description - suite = suite.parentSuite - - @time = new Date().getTime() - @description = stack.join(' -> ') - - reportSpecResults: -> - return unless @time? and @description? - @timedSpecs.push - description: @description - time: new Date().getTime() - @time - @time = null - @description = null - - reportRunnerResults: -> - console.log '10 longest running specs:' - for spec in _.sortBy(@timedSpecs, (spec) -> -spec.time)[0...10] - console.log "#{spec.time}ms" - console.log spec.description - jasmineEnv.addReporter(new TimeReporter()) jasmineEnv.specFilter = (spec) -> reporter.specFilter(spec) jasmineEnv.execute() From 339d29e1b52ec39a69db036788513b89db3c5e01 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 08:47:05 -0800 Subject: [PATCH 41/83] Search for something with fewer results Previously the search query used in the command panel spec returned 500+ matches when the specs needed far less to verify moving up and down and scrolling. This reduces the time take to run the command panel spec by ~2.5 seconds. --- src/packages/command-panel/spec/command-panel-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 884d9abe5..609e1a335 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -375,7 +375,7 @@ describe "CommandPanel", -> beforeEach -> previewList = commandPanel.previewList rootView.trigger 'command-panel:toggle' - waitsForPromise -> commandPanel.execute('X x/a/') + waitsForPromise -> commandPanel.execute('X x/sort/') describe "when move-down and move-up are triggered on the preview list", -> it "selects the next/previous operation (if there is one), and scrolls the list if needed", -> From 14d9fc5e45e19c08424f316720816bb9b75325bc Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 09:00:02 -0800 Subject: [PATCH 42/83] Search for a more specific string This spec only requires one match so don't search for something that has many matches and will take longer to display. Reduces the run time of this spec by ~1.5 seconds. --- src/packages/command-panel/spec/command-panel-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 609e1a335..83e69f075 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -127,7 +127,7 @@ describe "CommandPanel", -> describe "when the preview list is/was previously visible", -> beforeEach -> rootView.trigger 'command-panel:toggle' - waitsForPromise -> commandPanel.execute('X x/a+/') + waitsForPromise -> commandPanel.execute('X x/quicksort/') describe "when the command panel is visible", -> beforeEach -> From c3a993c1f66d4d319a09f7bde53921e75bde787a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 09:29:22 -0800 Subject: [PATCH 43/83] Support logging longest suites --- spec/time-reporter.coffee | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/time-reporter.coffee b/spec/time-reporter.coffee index c2d12870c..6546a68bd 100644 --- a/spec/time-reporter.coffee +++ b/spec/time-reporter.coffee @@ -4,6 +4,7 @@ module.exports = class TimeReporter extends jasmine.Reporter timedSpecs: [] + timedSuites: {} constructor: -> window.logLongestSpec = -> window.logLongestSpecs(1) @@ -13,11 +14,19 @@ class TimeReporter extends jasmine.Reporter console.log "#{spec.time}ms" console.log spec.description + window.logLongestSuite = -> window.logLongestSuites(1) + window.logLongestSuites = (number=10) => + console.log "#{number} longest running suites:" + suites = _.map(@timedSuites, (key, value) -> [value, key]) + for suite in _.sortBy(suites, (suite) => -suite[1])[0...number] + console.log suite[0], suite[1] + reportSpecStarting: (spec) -> stack = [spec.description] suite = spec.suite while suite stack.unshift suite.description + @suite = suite.description suite = suite.parentSuite @time = new Date().getTime() @@ -27,8 +36,14 @@ class TimeReporter extends jasmine.Reporter reportSpecResults: -> return unless @time? and @description? + + duration = new Date().getTime() - @time @timedSpecs.push description: @description - time: new Date().getTime() - @time + time: duration + if @timedSuites[@suite] + @timedSuites[@suite] += duration + else + @timedSuites[@suite] = duration @time = null @description = null From f63ed1035d116d91da7d321e9684333762ef0103 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 09:30:12 -0800 Subject: [PATCH 44/83] Correct typo in spec --- src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index aba066a63..c6f655bfe 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -19,7 +19,7 @@ describe 'FuzzyFinder', -> describe "file-finder behavior", -> describe "toggling", -> describe "when the root view's project has a path", -> - it "shows the FuzzyFinder or hides it nad returns focus to the active editor if it already showing", -> + it "shows the FuzzyFinder or hides it and returns focus to the active editor if it already showing", -> rootView.attachToDom() expect(rootView.find('.fuzzy-finder')).not.toExist() rootView.find('.editor').trigger 'editor:split-right' From d55dfc8a6fc8ad9f4d732cac8ad0b70a5f5a8e9a Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 09:31:23 -0800 Subject: [PATCH 45/83] AutoIndent is a config property instead of a syntax property --- spec/app/edit-session-spec.coffee | 27 ++++++++++++++++++++------- src/app/edit-session.coffee | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 9dd4375ab..3de078143 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1916,17 +1916,30 @@ describe "EditSession", -> editSession.buffer.reload() expect(editSession.getCursorScreenPosition()).toEqual [0,1] - describe "autoIndent", -> - describe "when editor.autoIndent returns true based on the EditSession's grammar scope", -> - it "auto indents lines", -> - syntax.addProperties("." + editSession.languageMode.grammar.scopeName, editor: autoIndent: true ) + describe "auto-indent", -> + describe "editor.autoIndent", -> + it "auto-indents newlines by default", -> editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n") expect(editSession.lineForBufferRow(2)).toBe " " - describe "when editor.autoIndent returns false based on the EditSession's grammar scope", -> - it "auto indents lines", -> - syntax.addProperties("." + editSession.languageMode.grammar.scopeName, editor: autoIndent: false ) + it "does not auto-indent newlines if editor.autoIndent is false", -> + config.set("editor.autoIndent", false) editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n") expect(editSession.lineForBufferRow(2)).toBe "" + + it "auto-indents calls to `indent` by default", -> + editSession.setCursorBufferPosition([1, 30]) + editSession.insertText("\n ") + expect(editSession.lineForBufferRow(2)).toBe " " + editSession.indent() + expect(editSession.lineForBufferRow(2)).toBe " " + + it "does not auto-indents calls to `indent` if editor.autoIndent is false", -> + config.set("editor.autoIndent", false) + editSession.setCursorBufferPosition([1, 30]) + editSession.insertText("\n ") + expect(editSession.lineForBufferRow(2)).toBe " " + editSession.indent() + expect(editSession.lineForBufferRow(2)).toBe " " diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 32c751966..e5d437a23 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -158,7 +158,7 @@ class EditSession logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) shouldAutoIndent: -> - syntax.getProperty(["." + @languageMode.grammar.scopeName], "editor.autoIndent") ? false + config.get("editor.autoIndent") ? true insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() From 4f0e2c1e9b26cfd034c0c9394c22a2176bb8d7c1 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 09:31:57 -0800 Subject: [PATCH 46/83] Add editor.autoIndentPastedText config option --- spec/app/edit-session-spec.coffee | 24 ++++++++++++++++++++++++ src/app/edit-session.coffee | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 3de078143..67a0ec47c 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1943,3 +1943,27 @@ describe "EditSession", -> expect(editSession.lineForBufferRow(2)).toBe " " editSession.indent() expect(editSession.lineForBufferRow(2)).toBe " " + + describe "editor.autoIndentPastedText", -> + it "does not auto-indent pasted text by default", -> + editSession.setCursorBufferPosition([2, 0]) + editSession.insertText("0\n 2\n 4\n") + editSession.getSelection().setBufferRange([[2,0], [5,0]]) + editSession.cutSelectedText() + + editSession.pasteText() + expect(editSession.lineForBufferRow(2)).toBe "0" + expect(editSession.lineForBufferRow(3)).toBe " 2" + expect(editSession.lineForBufferRow(4)).toBe " 4" + + it "auto-indents pasted text when editor.autoIndentPastedText is true", -> + config.set("editor.autoIndentPastedText", true) + editSession.setCursorBufferPosition([2, 0]) + editSession.insertText("0\n 2\n 4\n") + editSession.getSelection().setBufferRange([[2,0], [5,0]]) + editSession.cutSelectedText() + + editSession.pasteText() + expect(editSession.lineForBufferRow(2)).toBe " 0" + expect(editSession.lineForBufferRow(3)).toBe " 2" + expect(editSession.lineForBufferRow(4)).toBe " 4" diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index e5d437a23..9b08be236 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -160,6 +160,9 @@ class EditSession shouldAutoIndent: -> config.get("editor.autoIndent") ? true + shouldAutoIndentPastedText: -> + config.get("editor.autoIndentPastedText") ? false + insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.insertText(text, options) @@ -220,12 +223,14 @@ class EditSession selection.copy(maintainPasteboard) maintainPasteboard = true - pasteText: (options={})-> + pasteText: (options={}) -> options.normalizeIndent ?= true + options.autoIndent ?= @shouldAutoIndentPastedText() [text, metadata] = pasteboard.read() _.extend(options, metadata) if metadata + console.log options @insertText(text, options) undo: -> From 4ff737aa71bfff9508f832b1658e4c5b8ebcc60d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 09:40:27 -0800 Subject: [PATCH 47/83] Remove old strip-trailing-whitespace from extensions --- src/extensions/strip-trailing-whitespace.coffee | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/extensions/strip-trailing-whitespace.coffee diff --git a/src/extensions/strip-trailing-whitespace.coffee b/src/extensions/strip-trailing-whitespace.coffee deleted file mode 100644 index cb27068c0..000000000 --- a/src/extensions/strip-trailing-whitespace.coffee +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = - name: "strip trailing whitespace" - - activate: (rootView) -> - rootView.eachBuffer (buffer) -> - buffer.on 'before-save', -> - buffer.scan /[ \t]+$/g, (match, range, { replace }) -> - replace('') From d2521ca8b81ac5406e67455fe78d3bddc6c8be3d Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 09:41:33 -0800 Subject: [PATCH 48/83] Set editor.autoIndent to false for tests Auto-indenting makes it more difficult to write simple tests. So we turn it off. --- spec/app/edit-session-spec.coffee | 6 ++++-- spec/spec-helper.coffee | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 67a0ec47c..36ad90389 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1918,7 +1918,8 @@ describe "EditSession", -> describe "auto-indent", -> describe "editor.autoIndent", -> - it "auto-indents newlines by default", -> + it "auto-indents newlines if editor.autoIndent is undefined (the default)", -> + config.set("editor.autoIndent", undefined) editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n") expect(editSession.lineForBufferRow(2)).toBe " " @@ -1929,7 +1930,8 @@ describe "EditSession", -> editSession.insertText("\n") expect(editSession.lineForBufferRow(2)).toBe "" - it "auto-indents calls to `indent` by default", -> + it "auto-indents calls to `indent` if editor.autoIndent is undefined (the default)", -> + config.set("editor.autoIndent", undefined) editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n ") expect(editSession.lineForBufferRow(2)).toBe " " diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c15eb37b2..6f164e220 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -34,6 +34,7 @@ beforeEach -> spyOn(config, 'load') spyOn(config, 'save') config.set "editor.fontSize", 16 + config.set "editor.autoIndent", false # make editor display updates synchronous spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() From d53572d54d2232a00b450ebcbe937bda4b26a1ae Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 09:42:20 -0800 Subject: [PATCH 49/83] Rename editor.autoIndentPastedText to editor.autoIndentOnPaste --- spec/app/edit-session-spec.coffee | 6 +++--- src/app/edit-session.coffee | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 36ad90389..2ce626cfb 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1946,7 +1946,7 @@ describe "EditSession", -> editSession.indent() expect(editSession.lineForBufferRow(2)).toBe " " - describe "editor.autoIndentPastedText", -> + describe "editor.autoIndentOnPaste", -> it "does not auto-indent pasted text by default", -> editSession.setCursorBufferPosition([2, 0]) editSession.insertText("0\n 2\n 4\n") @@ -1958,8 +1958,8 @@ describe "EditSession", -> expect(editSession.lineForBufferRow(3)).toBe " 2" expect(editSession.lineForBufferRow(4)).toBe " 4" - it "auto-indents pasted text when editor.autoIndentPastedText is true", -> - config.set("editor.autoIndentPastedText", true) + it "auto-indents pasted text when editor.autoIndentOnPaste is true", -> + config.set("editor.autoIndentOnPaste", true) editSession.setCursorBufferPosition([2, 0]) editSession.insertText("0\n 2\n 4\n") editSession.getSelection().setBufferRange([[2,0], [5,0]]) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 9b08be236..1a20813cc 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -161,7 +161,7 @@ class EditSession config.get("editor.autoIndent") ? true shouldAutoIndentPastedText: -> - config.get("editor.autoIndentPastedText") ? false + config.get("editor.autoIndentOnPaste") ? false insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() @@ -230,7 +230,6 @@ class EditSession [text, metadata] = pasteboard.read() _.extend(options, metadata) if metadata - console.log options @insertText(text, options) undo: -> From a3f46ed184d6b1416a31f6ee805fcd0d00cc281a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 10:08:34 -0800 Subject: [PATCH 50/83] Remove command packages now handled by editor --- src/packages/lowercase-command/index.coffee | 1 - .../spec/lowercase-command-spec.coffee | 23 ------------------- .../src/lowercase-command.coffee | 8 ------- src/packages/uppercase-command/index.coffee | 1 - .../spec/uppercase-command-spec.coffee | 23 ------------------- .../src/uppercase-command.coffee | 8 ------- 6 files changed, 64 deletions(-) delete mode 100644 src/packages/lowercase-command/index.coffee delete mode 100644 src/packages/lowercase-command/spec/lowercase-command-spec.coffee delete mode 100644 src/packages/lowercase-command/src/lowercase-command.coffee delete mode 100644 src/packages/uppercase-command/index.coffee delete mode 100644 src/packages/uppercase-command/spec/uppercase-command-spec.coffee delete mode 100644 src/packages/uppercase-command/src/uppercase-command.coffee diff --git a/src/packages/lowercase-command/index.coffee b/src/packages/lowercase-command/index.coffee deleted file mode 100644 index 18f22efe6..000000000 --- a/src/packages/lowercase-command/index.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = require "./src/lowercase-command" diff --git a/src/packages/lowercase-command/spec/lowercase-command-spec.coffee b/src/packages/lowercase-command/spec/lowercase-command-spec.coffee deleted file mode 100644 index 120120f2d..000000000 --- a/src/packages/lowercase-command/spec/lowercase-command-spec.coffee +++ /dev/null @@ -1,23 +0,0 @@ -LowerCaseCommand = require 'lowercase-command' -RootView = require 'root-view' -fs = require 'fs' - -describe "LowerCaseCommand", -> - [rootView, editor, path] = [] - - beforeEach -> - rootView = new RootView - rootView.open(require.resolve 'fixtures/sample.js') - - rootView.focus() - editor = rootView.getActiveEditor() - - afterEach -> - rootView.remove() - - it "replaces the selected text with all lower case characters", -> - LowerCaseCommand.activate(rootView) - editor.setSelectedBufferRange([[11,14], [11,19]]) - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'Array' - editor.trigger 'lowercase' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'array' diff --git a/src/packages/lowercase-command/src/lowercase-command.coffee b/src/packages/lowercase-command/src/lowercase-command.coffee deleted file mode 100644 index 00f04158a..000000000 --- a/src/packages/lowercase-command/src/lowercase-command.coffee +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = -class LowerCaseCommand - - @activate: (rootView) -> - rootView.eachEditor (editor) -> - editor.bindToKeyedEvent 'meta-Y', 'lowercase', -> - editor.replaceSelectedText (text) -> - text.toLowerCase() diff --git a/src/packages/uppercase-command/index.coffee b/src/packages/uppercase-command/index.coffee deleted file mode 100644 index 55beed3cb..000000000 --- a/src/packages/uppercase-command/index.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = require "./src/uppercase-command" diff --git a/src/packages/uppercase-command/spec/uppercase-command-spec.coffee b/src/packages/uppercase-command/spec/uppercase-command-spec.coffee deleted file mode 100644 index 5ad77077e..000000000 --- a/src/packages/uppercase-command/spec/uppercase-command-spec.coffee +++ /dev/null @@ -1,23 +0,0 @@ -UpperCaseCommand = require 'uppercase-command' -RootView = require 'root-view' -fs = require 'fs' - -describe "UpperCaseCommand", -> - [rootView, editor, path] = [] - - beforeEach -> - rootView = new RootView - rootView.open(require.resolve 'fixtures/sample.js') - - rootView.focus() - editor = rootView.getActiveEditor() - - afterEach -> - rootView.remove() - - it "replaces the selected text with all upper case characters", -> - UpperCaseCommand.activate(rootView) - editor.setSelectedBufferRange([[0,0], [0,3]]) - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'var' - editor.trigger 'uppercase' - expect(editor.getTextInRange(editor.getSelection().getBufferRange())).toBe 'VAR' diff --git a/src/packages/uppercase-command/src/uppercase-command.coffee b/src/packages/uppercase-command/src/uppercase-command.coffee deleted file mode 100644 index c7cc02222..000000000 --- a/src/packages/uppercase-command/src/uppercase-command.coffee +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = -class UpperCaseCommand - - @activate: (rootView) -> - rootView.eachEditor (editor) -> - editor.bindToKeyedEvent 'meta-X', 'uppercase', -> - editor.replaceSelectedText (text) -> - text.toUpperCase() From ca41bf070922fe9e9246edffd91a4ee43f2f1239 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 10:28:24 -0800 Subject: [PATCH 51/83] Set auto-indent config defaults --- spec/app/edit-session-spec.coffee | 6 +++--- src/app/edit-session.coffee | 4 ++-- src/app/editor.coffee | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 2ce626cfb..10fbd3d7e 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1918,7 +1918,7 @@ describe "EditSession", -> describe "auto-indent", -> describe "editor.autoIndent", -> - it "auto-indents newlines if editor.autoIndent is undefined (the default)", -> + it "auto-indents newlines if editor.autoIndent is true", -> config.set("editor.autoIndent", undefined) editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n") @@ -1930,8 +1930,8 @@ describe "EditSession", -> editSession.insertText("\n") expect(editSession.lineForBufferRow(2)).toBe "" - it "auto-indents calls to `indent` if editor.autoIndent is undefined (the default)", -> - config.set("editor.autoIndent", undefined) + it "auto-indents calls to `indent` if editor.autoIndent is true", -> + config.set("editor.autoIndent", true) editSession.setCursorBufferPosition([1, 30]) editSession.insertText("\n ") expect(editSession.lineForBufferRow(2)).toBe " " diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 1a20813cc..8d7d3f03c 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -158,10 +158,10 @@ class EditSession logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) shouldAutoIndent: -> - config.get("editor.autoIndent") ? true + config.get("editor.autoIndent") shouldAutoIndentPastedText: -> - config.get("editor.autoIndentOnPaste") ? false + config.get("editor.autoIndentOnPaste") insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 40b64d64d..e8bb77dfd 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -17,6 +17,8 @@ class Editor extends View fontSize: 20 showInvisibles: false autosave: false + autoIndent: true + autoIndentOnPaste: false @content: (params) -> @div class: @classes(params), tabindex: -1, => From 0c0d48b8f60120c3fd674f3345d2e6aa8c4552e8 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jan 2013 10:40:05 -0800 Subject: [PATCH 52/83] Add commands to toggle auto-indent options --- src/app/root-view.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index c62bc7c90..801ecc7f2 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -102,6 +102,10 @@ class RootView extends View config.set("editor.showInvisibles", !config.get("editor.showInvisibles")) @command 'window:toggle-ignored-files', => config.set("core.hideGitIgnoredFiles", not config.core.hideGitIgnoredFiles) + @command 'window:toggle-auto-indent', => + config.set("editor.autoIndent", !config.get("editor.autoIndent")) + @command 'window:toggle-auto-indent-on-paste', => + config.set("editor.autoIndentOnPaste", !config.get("editor.autoIndentOnPaste")) afterAttach: (onDom) -> @focus() if onDom From 4dc7ade4e6b23c89310bfd57fea1bfa91fffdbf2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 10 Jan 2013 10:56:55 -0800 Subject: [PATCH 53/83] Globally mock pasteboard read and write in specs --- spec/app/edit-session-spec.coffee | 9 +++------ spec/spec-helper.coffee | 4 ++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 10fbd3d7e..7ee574e24 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1358,11 +1358,7 @@ describe "EditSession", -> expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.getTabLength() * 2] describe "pasteboard operations", -> - pasteboard = null beforeEach -> - pasteboard = 'first' - spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboard = text - spyOn($native, 'readFromPasteboard').andCallFake -> pasteboard editSession.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) describe ".cutSelectedText()", -> @@ -1381,7 +1377,7 @@ describe "EditSession", -> editSession.cutToEndOfLine() expect(buffer.lineForRow(2)).toBe ' if (items.length' expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(pasteboard).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' + expect(pasteboard.read()[0]).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' describe "when text is selected", -> it "only cuts the selected text, not to the end of the line", -> @@ -1391,7 +1387,7 @@ describe "EditSession", -> expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(pasteboard).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' + expect(pasteboard.read()[0]).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' describe ".copySelectedText()", -> it "copies selected text onto the clipboard", -> @@ -1402,6 +1398,7 @@ describe "EditSession", -> describe ".pasteText()", -> it "pastes text into the buffer", -> + pasteboard.write('first') editSession.pasteText() expect(editSession.buffer.lineForRow(0)).toBe "var first = function () {" expect(buffer.lineForRow(1)).toBe " var first = function(items) {" diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6f164e220..685004146 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -47,6 +47,10 @@ beforeEach -> TokenizedBuffer.prototype.chunkSize = Infinity spyOn(TokenizedBuffer.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk() + pasteboardContent = 'initial pasteboard content' + spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text + spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent + afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore From 3af97c4e3872e53de6950b301a1b596f3b02ab25 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 10 Jan 2013 11:34:17 -0800 Subject: [PATCH 54/83] Add Event.currentTargetView() and Event.targetView() --- spec/stdlib/jquery-extensions-spec.coffee | 32 ++++++++++++++++++++++- src/stdlib/jquery-extensions.coffee | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee index 21e8862a7..6b781370b 100644 --- a/spec/stdlib/jquery-extensions-spec.coffee +++ b/spec/stdlib/jquery-extensions-spec.coffee @@ -1,5 +1,5 @@ $ = require 'jquery' -{$$} = require 'space-pen' +{View, $$} = require 'space-pen' describe 'jQuery extensions', -> describe '$.fn.preempt(eventName, handler)', -> @@ -75,3 +75,33 @@ describe 'jQuery extensions', -> 'b2': "B2: Looks evil. Kinda is." 'a1': "A1: Waste perfectly-good steak" 'a2': null + + describe "Event.prototype", -> + class GrandchildView extends View + @content: -> @div class: 'grandchild' + + class ChildView extends View + @content: -> + @div class: 'child', => + @subview 'grandchild', new GrandchildView + + class ParentView extends View + @content: -> + @div class: 'parent', => + @subview 'child', new ChildView + + [parentView, event] = [] + beforeEach -> + parentView = new ParentView + eventHandler = jasmine.createSpy('eventHandler') + parentView.on 'foo', '.child', eventHandler + parentView.child.grandchild.trigger 'foo' + event = eventHandler.argsForCall[0][0] + + describe ".currentTargetView()", -> + it "returns the current target's space pen view", -> + expect(event.currentTargetView()).toBe parentView.child + + describe ".targetView()", -> + it "returns the target's space pen view", -> + expect(event.targetView()).toBe parentView.child.grandchild diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee index 53a851e28..c90fa6763 100644 --- a/src/stdlib/jquery-extensions.coffee +++ b/src/stdlib/jquery-extensions.coffee @@ -74,3 +74,5 @@ $.fn.command = (args...) -> @on(args...) $.Event.prototype.abortKeyBinding = -> +$.Event.prototype.currentTargetView = -> $(this.currentTarget).view() +$.Event.prototype.targetView = -> $(this.target).view() From 196a013cbcfd2f14426f3c08e163ab207264cf1e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 13:37:49 -0800 Subject: [PATCH 55/83] Remove unneded focus() call focus() is called from attach() --- src/packages/tree-view/src/tree-view.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index c3c57bfc2..cffdaa42f 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -135,7 +135,6 @@ class TreeView extends ScrollView revealActiveFile: -> @attach() - @focus() return unless activeFilePath = @rootView.getActiveEditor()?.getPath() From 1316e2136cf4684a22538ad4df1fa0247d1cd086 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 13:42:48 -0800 Subject: [PATCH 56/83] Don't attach the TreeView when the project has no path --- src/packages/tree-view/spec/tree-view-spec.coffee | 7 ++++++- src/packages/tree-view/src/tree-view.coffee | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 2606ba92f..432870480 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -54,7 +54,12 @@ describe "TreeView", -> atom.loadPackage 'tree-view' treeView = TreeView.instance - it "does not attach or create a root node", -> + it "does not attach to the root view or create a root node when initialized", -> + expect(treeView.hasParent()).toBeFalsy() + expect(treeView.root).not.toExist() + + it "does not attach to the root view or create a root node when attach() is called", -> + treeView.attach() expect(treeView.hasParent()).toBeFalsy() expect(treeView.root).not.toExist() diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index cffdaa42f..f1c9902cd 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -96,6 +96,7 @@ class TreeView extends ScrollView @attach() attach: -> + return unless rootView.project.getPath() @rootView.horizontal.prepend(this) @focus() From 6d914cdc7a918ef96211219c2b0c18ad48f5ca5e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 14:08:58 -0800 Subject: [PATCH 57/83] Don't attach the TreeView if RootView's path to open is a file --- src/app/root-view.coffee | 4 +++- src/packages/tree-view/spec/tree-view-spec.coffee | 12 ++++++++++++ src/packages/tree-view/src/tree-view.coffee | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 7cebe2c93..aa8b9a24f 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -36,6 +36,7 @@ class RootView extends View packageModules: null packageStates: null title: null + pathToOpenIsFile: false initialize: (projectOrPathToOpen, { @packageStates, suppressOpen } = {}) -> window.rootView = this @@ -48,11 +49,12 @@ class RootView extends View @project = new Project(projectOrPathToOpen) else @project = projectOrPathToOpen + @pathToOpenIsFile = pathToOpen and fs.isFile(pathToOpen) config.load() if pathToOpen - @open(pathToOpen) if fs.isFile(pathToOpen) and not suppressOpen + @open(pathToOpen) if @pathToOpenIsFile and not suppressOpen else @open() diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 432870480..f323100d7 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -86,6 +86,18 @@ describe "TreeView", -> expect(treeView.root.getPath()).toBe require.resolve('/tmp') expect(treeView.root.parent()).toMatchSelector(".tree-view") + describe "when the root view is opened to a file path", -> + beforeEach -> + rootView.deactivate() + + rootView = new RootView(require.resolve('fixtures/tree-view/tree-view.js')) + atom.loadPackage 'tree-view' + treeView = TreeView.instance + + it "does not attach to the root view but does create a root node when initialized", -> + expect(treeView.hasParent()).toBeFalsy() + expect(treeView.root).toExist() + describe "serialization", -> [newRootView, newTreeView] = [] diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index f1c9902cd..af7a893f0 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -18,7 +18,7 @@ class TreeView extends ScrollView @instance = new TreeView(rootView) if rootView.project.getPath() - @instance.attach() + @instance.attach() unless rootView.pathToOpenIsFile else rootView.project.one "path-changed", => @instance.attach() From e76d6808b7c218494aacccab7b919f385db3f5da Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 14:24:39 -0800 Subject: [PATCH 58/83] Upgrade to latest octicons --- static/atom.css | 2 +- static/octicons-regular-webfont.ttf | Bin 53336 -> 0 bytes static/octicons-regular-webfont.woff | Bin 0 -> 24640 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 static/octicons-regular-webfont.ttf create mode 100644 static/octicons-regular-webfont.woff diff --git a/static/atom.css b/static/atom.css index dd94ac496..13a207fee 100644 --- a/static/atom.css +++ b/static/atom.css @@ -87,7 +87,7 @@ html, body { @font-face { font-family: 'Octicons Regular'; - src: url(octicons-regular-webfont.ttf) format(truetype); + src: url("octicons-regular-webfont.woff") format("woff"); font-weight: normal; font-style: normal; } diff --git a/static/octicons-regular-webfont.ttf b/static/octicons-regular-webfont.ttf deleted file mode 100644 index 0c956f79342291a5281f9e32696507399f723de6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53336 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�ejux^x*OvLyDz>iQ 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 diff --git a/static/octicons-regular-webfont.woff b/static/octicons-regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..0e9c3f1eefa190d9be0a843ae6cb6283b2df6d53 GIT binary patch literal 24640 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 Date: Thu, 10 Jan 2013 15:30:56 -0800 Subject: [PATCH 59/83] Run specs from all package directory paths --- spec/spec-suite.coffee | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/spec-suite.coffee b/spec/spec-suite.coffee index 4e42fa2d7..f888d8a0d 100644 --- a/spec/spec-suite.coffee +++ b/spec/spec-suite.coffee @@ -1,11 +1,13 @@ fs = require 'fs' require 'spec-helper' + # Run core specs for path in fs.listTree(require.resolve("spec")) when /-spec\.coffee$/.test path require path # Run extension specs -for packagePath in fs.listTree(require.resolve("src/packages")) - for path in fs.listTree(fs.join(packagePath, "spec")) when /-spec\.coffee$/.test path - require path +for packageDirPath in config.packageDirPaths + for packagePath in fs.listTree(packageDirPath) + for path in fs.listTree(fs.join(packagePath, "spec")) when /-spec\.coffee$/.test path + require path From 63dbc2f783f686d1404411ba30c9fd2911d11ab1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 10 Jan 2013 15:33:30 -0800 Subject: [PATCH 60/83] Remove double spies on native pasteboard methods --- spec/app/pasteboard-spec.coffee | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/spec/app/pasteboard-spec.coffee b/spec/app/pasteboard-spec.coffee index 8dc6624ff..3cf2b991d 100644 --- a/spec/app/pasteboard-spec.coffee +++ b/spec/app/pasteboard-spec.coffee @@ -1,17 +1,10 @@ describe "Pasteboard", -> - nativePasteboard = null - beforeEach -> - nativePasteboard = 'first' - spyOn($native, 'writeToPasteboard').andCallFake (text) -> nativePasteboard = text - spyOn($native, 'readFromPasteboard').andCallFake -> nativePasteboard - describe "write(text, metadata) and read()", -> it "writes and reads text to/from the native pasteboard", -> - expect(pasteboard.read()).toEqual ['first'] + expect(pasteboard.read()).toEqual ['initial pasteboard content'] pasteboard.write('next') - expect(nativePasteboard).toBe 'next' + expect(pasteboard.read()[0]).toBe 'next' it "returns metadata if the item on the native pasteboard matches the last written item", -> pasteboard.write('next', {meta: 'data'}) - expect(nativePasteboard).toBe 'next' expect(pasteboard.read()).toEqual ['next', {meta: 'data'}] From 8bf16ba60272cd15c334d935e4ea24c07cd49b57 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 15:39:32 -0800 Subject: [PATCH 61/83] Don't attach the tree view when a project path changes The tree view shouldn't automatically open when the project first gets a path after not having one. It can be still be toggled to be opened once the project has a path. --- .../tree-view/spec/tree-view-spec.coffee | 17 ++--------------- src/packages/tree-view/src/tree-view.coffee | 7 ++----- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index f323100d7..0ead23590 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -66,23 +66,10 @@ describe "TreeView", -> it "serializes without throwing an exception", -> expect(-> treeView.serialize()).not.toThrow() - describe "when the project is assigned a path because a buffer is opened", -> - it "attaches the tree and creates a root directory view", -> - rootView.open(require.resolve('fixtures/sample.js')) - expect(treeView.hasParent()).toBeTruthy() - expect(treeView.root.getPath()).toBe require.resolve('fixtures') - expect(treeView.root.parent()).toMatchSelector(".tree-view") - - oldRoot = treeView.root - - rootView.project.setPath('/tmp') - expect(treeView.root).not.toEqual oldRoot - expect(oldRoot.hasParent()).toBeFalsy() - describe "when the project is assigned a path because a new buffer is saved", -> - it "attaches the tree and creates a root directory view", -> + it "creates a root directory view but does not attach to the root view", -> rootView.getActiveEditSession().saveAs("/tmp/test.txt") - expect(treeView.hasParent()).toBeTruthy() + expect(treeView.hasParent()).toBeFalsy() expect(treeView.root.getPath()).toBe require.resolve('/tmp') expect(treeView.root.parent()).toMatchSelector(".tree-view") diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index af7a893f0..a07a4bb7a 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -17,11 +17,8 @@ class TreeView extends ScrollView else @instance = new TreeView(rootView) - if rootView.project.getPath() - @instance.attach() unless rootView.pathToOpenIsFile - else - rootView.project.one "path-changed", => - @instance.attach() + if rootView.project.getPath() and not rootView.pathToOpenIsFile + @instance.attach() @deactivate: -> @instance.deactivate() From d27080cee65b3befe10be54a4f57b41b338be09c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 16:24:36 -0800 Subject: [PATCH 62/83] Always set pathToOpen in RootView.initialize Without this an untitled buffer will be opened when Atom is reopened after being closed with no editors open. --- spec/app/root-view-spec.coffee | 93 ++++++++++++++++++++-------------- src/app/root-view.coffee | 1 + 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index baad07109..e5abf32a0 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -69,52 +69,67 @@ describe "RootView", -> path = require.resolve 'fixtures' rootView.remove() rootView = new RootView(path) - rootView.open('dir/a') - editor1 = rootView.getActiveEditor() - editor2 = editor1.splitRight() - editor3 = editor2.splitRight() - editor4 = editor2.splitDown() - editor2.edit(rootView.project.buildEditSessionForPath('dir/b')) - editor3.edit(rootView.project.buildEditSessionForPath('sample.js')) - editor3.setCursorScreenPosition([2, 4]) - editor4.edit(rootView.project.buildEditSessionForPath('sample.txt')) - editor4.setCursorScreenPosition([0, 2]) - rootView.attachToDom() - editor2.focus() - viewState = rootView.serialize() - rootView.remove() + describe "when there are open editors", -> + beforeEach -> + rootView.open('dir/a') + editor1 = rootView.getActiveEditor() + editor2 = editor1.splitRight() + editor3 = editor2.splitRight() + editor4 = editor2.splitDown() + editor2.edit(rootView.project.buildEditSessionForPath('dir/b')) + editor3.edit(rootView.project.buildEditSessionForPath('sample.js')) + editor3.setCursorScreenPosition([2, 4]) + editor4.edit(rootView.project.buildEditSessionForPath('sample.txt')) + editor4.setCursorScreenPosition([0, 2]) + rootView.attachToDom() + editor2.focus() + viewState = rootView.serialize() + rootView.remove() - it "constructs the view with the same project and panes", -> - rootView = RootView.deserialize(viewState) - rootView.attachToDom() + it "constructs the view with the same project and panes", -> + rootView = RootView.deserialize(viewState) + rootView.attachToDom() - expect(rootView.getEditors().length).toBe 4 - editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view() - editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view() - editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view() - editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view() + expect(rootView.getEditors().length).toBe 4 + editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view() + editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view() + editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view() + editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view() - expect(editor1.getPath()).toBe require.resolve('fixtures/dir/a') - expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b') - expect(editor3.getPath()).toBe require.resolve('fixtures/sample.js') - expect(editor3.getCursorScreenPosition()).toEqual [2, 4] - expect(editor4.getPath()).toBe require.resolve('fixtures/sample.txt') - expect(editor4.getCursorScreenPosition()).toEqual [0, 2] + expect(editor1.getPath()).toBe require.resolve('fixtures/dir/a') + expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b') + expect(editor3.getPath()).toBe require.resolve('fixtures/sample.js') + expect(editor3.getCursorScreenPosition()).toEqual [2, 4] + expect(editor4.getPath()).toBe require.resolve('fixtures/sample.txt') + expect(editor4.getCursorScreenPosition()).toEqual [0, 2] - # ensure adjust pane dimensions is called - expect(editor1.width()).toBeGreaterThan 0 - expect(editor2.width()).toBeGreaterThan 0 - expect(editor3.width()).toBeGreaterThan 0 - expect(editor4.width()).toBeGreaterThan 0 + # ensure adjust pane dimensions is called + expect(editor1.width()).toBeGreaterThan 0 + expect(editor2.width()).toBeGreaterThan 0 + expect(editor3.width()).toBeGreaterThan 0 + expect(editor4.width()).toBeGreaterThan 0 - # ensure correct editor is focused again - expect(editor2.isFocused).toBeTruthy() - expect(editor1.isFocused).toBeFalsy() - expect(editor3.isFocused).toBeFalsy() - expect(editor4.isFocused).toBeFalsy() + # ensure correct editor is focused again + expect(editor2.isFocused).toBeTruthy() + expect(editor1.isFocused).toBeFalsy() + expect(editor3.isFocused).toBeFalsy() + expect(editor4.isFocused).toBeFalsy() + + expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{rootView.project.getPath()}" + + describe "where there are no open editors", -> + beforeEach -> + rootView.attachToDom() + viewState = rootView.serialize() + rootView.remove() + + it "constructs the view with no open editors", -> + rootView = RootView.deserialize(viewState) + rootView.attachToDom() + + expect(rootView.getEditors().length).toBe 0 - expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{rootView.project.getPath()}" describe "when called with no pathToOpen", -> it "opens an empty buffer", -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index aa8b9a24f..99cf87eb6 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -49,6 +49,7 @@ class RootView extends View @project = new Project(projectOrPathToOpen) else @project = projectOrPathToOpen + pathToOpen = @project?.getPath() @pathToOpenIsFile = pathToOpen and fs.isFile(pathToOpen) config.load() From 2086c2b3533bd83bc94cfd88d3fcd10c5d84720c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 Jan 2013 15:41:23 -0700 Subject: [PATCH 63/83] Fix point snippet prefix --- .atom/snippets/coffee.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.atom/snippets/coffee.cson b/.atom/snippets/coffee.cson index ff978671c..5421f12f4 100644 --- a/.atom/snippets/coffee.cson +++ b/.atom/snippets/coffee.cson @@ -40,5 +40,5 @@ prefix: ":" body: '${1:"${2:key}"}: ${3:value}' "Create Jasmine spy": - prefix: "pt" + prefix: "spy" body: 'jasmine.createSpy("${1:description}")$2' From b0fe034c9a4a722bc2a9cff544fb1eb73889b885 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 Jan 2013 16:54:09 -0700 Subject: [PATCH 64/83] Add `autoflow` package w/ `autoflow:reflow-paragraph` command --- spec/app/edit-session-spec.coffee | 30 ++++++++++++++ src/app/cursor.coffee | 17 ++++++++ src/app/edit-session.coffee | 4 ++ src/app/editor.coffee | 1 + src/packages/autoflow/index.coffee | 1 + src/packages/autoflow/lib/autoflow.coffee | 31 ++++++++++++++ .../autoflow/spec/autoflow-spec.coffee | 41 +++++++++++++++++++ 7 files changed, 125 insertions(+) create mode 100644 src/packages/autoflow/index.coffee create mode 100644 src/packages/autoflow/lib/autoflow.coffee create mode 100644 src/packages/autoflow/spec/autoflow-spec.coffee diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 7ee574e24..2adf7feb4 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -307,6 +307,36 @@ describe "EditSession", -> editSession.moveCursorToEndOfWord() expect(editSession.getCursorBufferPosition()).toEqual endPosition + describe ".getCurrentParagraphBufferRange()", -> + it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> + buffer.setText """ + I am the first paragraph, + bordered by the beginning of + the file + #{' '} + + I am the second paragraph + with blank lines above and below + me. + + I am the last paragraph, + bordered by the end of the file. + """ + + # in a paragraph + editSession.setCursorBufferPosition([1, 7]) + expect(editSession.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] + + editSession.setCursorBufferPosition([7, 1]) + expect(editSession.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] + + editSession.setCursorBufferPosition([9, 10]) + expect(editSession.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] + + # between paragraphs + editSession.setCursorBufferPosition([3, 1]) + expect(editSession.getCurrentParagraphBufferRange()).toBeUndefined() + describe "selection", -> selection = null diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index da44e890e..498b2ef83 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -175,6 +175,23 @@ class Cursor getCurrentLineBufferRange: (options) -> @editSession.bufferRangeForBufferRow(@getBufferRow(), options) + getCurrentParagraphBufferRange: -> + row = @getBufferRow() + return unless /\w/.test(@editSession.lineForBufferRow(row)) + + startRow = row + while startRow > 0 + break unless /\w/.test(@editSession.lineForBufferRow(startRow - 1)) + startRow-- + + endRow = row + lastRow = @editSession.getLastBufferRow() + while endRow < lastRow + break unless /\w/.test(@editSession.lineForBufferRow(endRow + 1)) + endRow++ + + new Range([startRow, 0], [endRow, @editSession.lineLengthForBufferRow(endRow)]) + getCurrentWordPrefix: -> @editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 8d7d3f03c..24bcb6bf1 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -139,6 +139,7 @@ class EditSession getLastBufferRow: -> @buffer.getLastRow() bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) lineForBufferRow: (row) -> @buffer.lineForRow(row) + lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) scanInRange: (args...) -> @buffer.scanInRange(args...) backwardsScanInRange: (args...) -> @buffer.backwardsScanInRange(args...) @@ -489,6 +490,9 @@ class EditSession getTextInBufferRange: (range) -> @buffer.getTextInRange(range) + getCurrentParagraphBufferRange: -> + @getCursor().getCurrentParagraphBufferRange() + moveCursorUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index e8bb77dfd..4f2b2befb 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -206,6 +206,7 @@ class Editor extends View getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() + getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() getSelection: (index) -> @activeEditSession.getSelection(index) getSelections: -> @activeEditSession.getSelections() diff --git a/src/packages/autoflow/index.coffee b/src/packages/autoflow/index.coffee new file mode 100644 index 000000000..950ab2727 --- /dev/null +++ b/src/packages/autoflow/index.coffee @@ -0,0 +1 @@ +module.exports = require './lib/autoflow' diff --git a/src/packages/autoflow/lib/autoflow.coffee b/src/packages/autoflow/lib/autoflow.coffee new file mode 100644 index 000000000..41a583e68 --- /dev/null +++ b/src/packages/autoflow/lib/autoflow.coffee @@ -0,0 +1,31 @@ +module.exports = + activate: (rootView) -> + rootView.command 'autoflow:reflow-paragraph', '.editor', (e) => + @reflowParagraph(e.currentTargetView()) + + reflowParagraph: (editor) -> + if range = editor.getCurrentParagraphBufferRange() + editor.getBuffer().change(range, @reflow(editor.getTextInRange(range))) + + reflow: (text) -> + wrapColumn = config.get('editor.preferredLineLength') ? 80 + lines = [] + + currentLine = [] + currentLineLength = 0 + for segment in @segmentText(text.replace(/\n/g, ' ')) + if /\w/.test(segment) and currentLineLength + segment.length > wrapColumn + lines.push(currentLine.join('')) + currentLine = [] + currentLineLength = 0 + currentLine.push(segment) + currentLineLength += segment.length + lines.push(currentLine.join('')) + + lines.join('\n').replace(/\s+\n/g, '\n') + + segmentText: (text) -> + segments = [] + re = /[\s]+|[^\s]+/g + segments.push(match[0]) while match = re.exec(text) + segments diff --git a/src/packages/autoflow/spec/autoflow-spec.coffee b/src/packages/autoflow/spec/autoflow-spec.coffee new file mode 100644 index 000000000..d41e25bb1 --- /dev/null +++ b/src/packages/autoflow/spec/autoflow-spec.coffee @@ -0,0 +1,41 @@ +RootView = require 'root-view' + +describe "Autoflow package", -> + editor = null + + beforeEach -> + rootView = new RootView + atom.loadPackage 'autoflow' + editor = rootView.getActiveEditor() + + describe "autoflow:reflow-paragraph", -> + it "rearranges line breaks in the current paragraph to ensure lines are shorter than config.editor.preferredLineLength", -> + config.set('editor.preferredLineLength', 30) + editor.setText """ + This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph. + + The quick brown fox jumps over the lazy + dog. The preceding sentence contains every letter + in the entire English alphabet, which has absolutely no relevance + to this test. + + This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph. + + """ + + editor.setCursorBufferPosition([3, 5]) + editor.trigger 'autoflow:reflow-paragraph' + + expect(editor.getText()).toBe """ + This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph. + + The quick brown fox jumps over + the lazy dog. The preceding + sentence contains every letter + in the entire English + alphabet, which has absolutely + no relevance to this test. + + This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph. + + """ From b307bcc0de9e41ae6c8da32d0a248885d1d5d8e5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 Jan 2013 17:21:23 -0700 Subject: [PATCH 65/83] Handle single long words that exceed the wrap column in autoflow --- src/packages/autoflow/lib/autoflow.coffee | 10 ++++++---- src/packages/autoflow/spec/autoflow-spec.coffee | 14 +++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/packages/autoflow/lib/autoflow.coffee b/src/packages/autoflow/lib/autoflow.coffee index 41a583e68..a1dc9f73b 100644 --- a/src/packages/autoflow/lib/autoflow.coffee +++ b/src/packages/autoflow/lib/autoflow.coffee @@ -14,10 +14,12 @@ module.exports = currentLine = [] currentLineLength = 0 for segment in @segmentText(text.replace(/\n/g, ' ')) - if /\w/.test(segment) and currentLineLength + segment.length > wrapColumn - lines.push(currentLine.join('')) - currentLine = [] - currentLineLength = 0 + if /\w/.test(segment) and + (currentLineLength + segment.length > wrapColumn) and + (currentLineLength > 0 or segment.length < wrapColumn) + lines.push(currentLine.join('')) + currentLine = [] + currentLineLength = 0 currentLine.push(segment) currentLineLength += segment.length lines.push(currentLine.join('')) diff --git a/src/packages/autoflow/spec/autoflow-spec.coffee b/src/packages/autoflow/spec/autoflow-spec.coffee index d41e25bb1..206172843 100644 --- a/src/packages/autoflow/spec/autoflow-spec.coffee +++ b/src/packages/autoflow/spec/autoflow-spec.coffee @@ -7,10 +7,10 @@ describe "Autoflow package", -> rootView = new RootView atom.loadPackage 'autoflow' editor = rootView.getActiveEditor() + config.set('editor.preferredLineLength', 30) describe "autoflow:reflow-paragraph", -> it "rearranges line breaks in the current paragraph to ensure lines are shorter than config.editor.preferredLineLength", -> - config.set('editor.preferredLineLength', 30) editor.setText """ This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph. @@ -39,3 +39,15 @@ describe "Autoflow package", -> This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph. """ + + it "allows for single words that exceed the preferred wrap column length", -> + editor.setText("this-is-a-super-long-word-that-shouldn't-break-autoflow and these are some smaller words") + + editor.setCursorBufferPosition([0, 4]) + editor.trigger 'autoflow:reflow-paragraph' + + expect(editor.getText()).toBe """ + this-is-a-super-long-word-that-shouldn't-break-autoflow + and these are some smaller + words + """ From ac3c0592468692a4c93fed4fe8d4478c63949f9d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 10 Jan 2013 18:02:55 -0700 Subject: [PATCH 66/83] Extract `_.setValueForKeyPath` to underscore extensions --- src/app/config.coffee | 9 +-------- src/stdlib/underscore-extensions.coffee | 8 ++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/config.coffee b/src/app/config.coffee index d006e7540..448a7fc2d 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -47,14 +47,7 @@ class Config _.valueForKeyPath(@defaultSettings, keyPath) set: (keyPath, value) -> - keys = keyPath.split('.') - hash = @settings - while keys.length > 1 - key = keys.shift() - hash[key] ?= {} - hash = hash[key] - hash[keys.shift()] = value - + _.setValueForKeyPath(@settings, keyPath, value) @update() value diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee index 41fa22764..be1245349 100644 --- a/src/stdlib/underscore-extensions.coffee +++ b/src/stdlib/underscore-extensions.coffee @@ -99,6 +99,14 @@ _.mixin return unless object? object + setValueForKeyPath: (object, keyPath, value) -> + keys = keyPath.split('.') + while keys.length > 1 + key = keys.shift() + object[key] ?= {} + object = object[key] + object[keys.shift()] = value + compactObject: (object) -> newObject = {} for key, value of object From 2fb27bb2dd1f0bfa0586a9dc5b1e5ef11551d02b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 10 Jan 2013 18:04:22 -0700 Subject: [PATCH 67/83] Store `pathToOpen` using new `atom.set/getWindowState` api When you use `setWindowState`, your data is saved across refreshes. You can only store state that can be serialized to JSON. --- native/v8_extensions/native.h | 7 +++++-- native/v8_extensions/native.js | 6 ++++++ native/v8_extensions/native.mm | 11 +++++++++++ src/app/atom.coffee | 13 +++++++++++++ src/app/window.coffee | 6 ++++-- src/window-bootstrap.coffee | 4 +++- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/native/v8_extensions/native.h b/native/v8_extensions/native.h index 1031be7ec..e7a200493 100644 --- a/native/v8_extensions/native.h +++ b/native/v8_extensions/native.h @@ -7,15 +7,18 @@ namespace v8_extensions { class Native : public CefV8Handler { public: Native(); - + virtual bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) OVERRIDE; - + // Provide the reference counting implementation for this class. IMPLEMENT_REFCOUNTING(Native); + +private: + std::string windowState; }; } diff --git a/native/v8_extensions/native.js b/native/v8_extensions/native.js index 85cdf5be8..9124d89fc 100644 --- a/native/v8_extensions/native.js +++ b/native/v8_extensions/native.js @@ -76,4 +76,10 @@ var $native = {}; native function getPlatform(); $native.getPlatform = getPlatform; + native function setWindowState(state); + $native.setWindowState = setWindowState; + + native function getWindowState(); + $native.getWindowState = getWindowState; + })(); diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index c947aec01..0a66591ca 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -31,6 +31,7 @@ void throwException(const CefRefPtr& global, CefRefPtrGetStringValue().ToString(); + return true; + } + + else if (name == "getWindowState") { + retval = CefV8Value::CreateString(windowState); + return true; + } + return false; }; diff --git a/src/app/atom.coffee b/src/app/atom.coffee index 96594961e..8f02a3db0 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -109,3 +109,16 @@ _.extend atom, if name is 'reply' [messageId, callbackIndex] = data.shift() @pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...) + + setWindowState: (keyPath, value) -> + windowState = @getWindowState() + _.setValueForKeyPath(windowState, keyPath, value) + $native.setWindowState(JSON.stringify(windowState)) + windowState + + getWindowState: (keyPath) -> + windowState = JSON.parse($native.getWindowState()) + if keyPath + _.valueForKeyPath(windowState, keyPath) + else + windowState diff --git a/src/app/window.coffee b/src/app/window.coffee index 85077e84a..4d0b6e6a7 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -47,8 +47,10 @@ windowAdditions = false shutdown: -> - @rootView?.deactivate() - @rootView = null + if @rootView + atom.setWindowState('pathToOpen', @rootView.project.getPath()) + @rootView.deactivate() + @rootView = null $(window).unbind('focus') $(window).unbind('blur') $(window).off('before') diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index 86404bb98..5720d9af9 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -1,4 +1,6 @@ # Like sands through the hourglass, so are the days of our lives. require 'atom' require 'window' -window.attachRootView(window.location.params.pathToOpen) + +pathToOpen = atom.getWindowState('pathToOpen') ? window.location.params.pathToOpen +window.attachRootView(pathToOpen) From 3a582eab63f823f3837e2f9123c974dfdaf5da74 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 10 Jan 2013 17:45:24 -0800 Subject: [PATCH 68/83] Display editor's grammar name in status bar Clicking on the grammar name displays the list of available grammars that can be switched to. --- spec/app/editor-spec.coffee | 15 +++++++++++++++ src/app/editor.coffee | 1 + .../status-bar/spec/status-bar-spec.coffee | 18 ++++++++++++++++++ src/packages/status-bar/src/status-bar.coffee | 8 +++++++- .../status-bar/stylesheets/status-bar.css | 9 ++++++++- 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index ad79d4e18..ae616f814 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -2137,6 +2137,21 @@ describe "Editor", -> expect(editor.updateDisplay).not.toHaveBeenCalled() expect(editor.getGrammar().name).toBe 'JavaScript' + it "emits an editor:grammar-changed event when updated", -> + rootView.open(path) + editor = rootView.getActiveEditor() + eventHandler = jasmine.createSpy('eventHandler') + editor.on('editor:grammar-changed', eventHandler) + editor.reloadGrammar() + + expect(eventHandler).not.toHaveBeenCalled() + + jsGrammar = syntax.grammarForFilePath('/tmp/js.js') + rootView.project.addGrammarOverrideForPath(path, jsGrammar) + editor.reloadGrammar() + + expect(eventHandler).toHaveBeenCalled() + describe ".replaceSelectedText()", -> it "doesn't call the replace function when the selection is empty", -> replaced = false diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 4f2b2befb..5f5aaa831 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -1128,6 +1128,7 @@ class Editor extends View if grammarChanged @clearRenderedLines() @updateDisplay() + @trigger 'editor:grammar-changed' grammarChanged bindToKeyedEvent: (key, event, callback) -> diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index d990b6a7a..d3c9a2b7f 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -176,3 +176,21 @@ describe "StatusBar", -> it "displays the diff stat for new files", -> rootView.open(newPath) expect(statusBar.gitStatusIcon).toHaveText('+1') + + describe "grammar label", -> + it "displays the name of the current grammar", -> + expect(statusBar.find('.grammar-name').text()).toBe 'JavaScript' + + describe "when the editor's grammar changes", -> + it "displays the new grammar of the editor", -> + textGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'Plain Text' + rootView.project.addGrammarOverrideForPath(editor.getPath(), textGrammar) + editor.reloadGrammar() + expect(statusBar.find('.grammar-name').text()).toBe textGrammar.name + + describe "when clicked", -> + it "toggles the editor:select-grammar event", -> + eventHandler = jasmine.createSpy('eventHandler') + editor.on 'editor:select-grammar', eventHandler + statusBar.find('.grammar-name').click() + expect(eventHandler).toHaveBeenCalled() diff --git a/src/packages/status-bar/src/status-bar.coffee b/src/packages/status-bar/src/status-bar.coffee index feee92e4f..0836898d3 100644 --- a/src/packages/status-bar/src/status-bar.coffee +++ b/src/packages/status-bar/src/status-bar.coffee @@ -25,7 +25,7 @@ class StatusBar extends View @span class: 'current-path', outlet: 'currentPath' @span class: 'buffer-modified', outlet: 'bufferModified' @span class: 'cursor-position', outlet: 'cursorPosition' - + @span class: 'grammar-name', outlet: 'grammarName' initialize: (@rootView, @editor) -> @updatePathText() @@ -36,6 +36,8 @@ class StatusBar extends View @updateCursorPositionText() @subscribe @editor, 'cursor:moved', => @updateCursorPositionText() @subscribe $(window), 'focus', => @updateStatusBar() + @subscribe @grammarName, 'click', => @editor.trigger 'editor:select-grammar' + @subscribe @editor, 'editor:grammar-changed', => @updateGrammarText() @subscribeToBuffer() @@ -48,10 +50,14 @@ class StatusBar extends View @updateStatusBar() updateStatusBar: -> + @updateGrammarText() @updateBranchText() @updateBufferHasModifiedText(@buffer.isModified()) @updateStatusText() + updateGrammarText: -> + @grammarName.text(@editor.getGrammar().name) + updateBufferHasModifiedText: (differsFromDisk)-> if differsFromDisk @bufferModified.text('*') unless @isModified diff --git a/src/packages/status-bar/stylesheets/status-bar.css b/src/packages/status-bar/stylesheets/status-bar.css index 14ba139c7..e15105d5e 100644 --- a/src/packages/status-bar/stylesheets/status-bar.css +++ b/src/packages/status-bar/stylesheets/status-bar.css @@ -6,12 +6,19 @@ line-height: 14px; color: #969696; position: relative; + -webkit-user-select: none; + cursor: default; } -.status-bar .cursor-position { +.status-bar .cursor-position, +.status-bar .grammar-name { padding-left: 10px; } +.status-bar .grammar-name { + cursor: pointer; +} + .status-bar .git-branch { float: right; } From 30fc2536e0cdad4a85806c7600b21c6d2a534a2e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 08:29:50 -0800 Subject: [PATCH 69/83] Ignore basic core move events in heatmap --- src/packages/command-logger/src/command-logger.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/packages/command-logger/src/command-logger.coffee b/src/packages/command-logger/src/command-logger.coffee index 29097e8df..4011faef7 100644 --- a/src/packages/command-logger/src/command-logger.coffee +++ b/src/packages/command-logger/src/command-logger.coffee @@ -24,6 +24,10 @@ class CommandLogger extends ScrollView 'core:cancel' 'core:confirm' 'core:delete' + 'core:move-down' + 'core:move-left' + 'core:move-right' + 'core:move-up' 'editor:newline' 'tree-view:directory-modified' ] From 07fd29ccc43626f096ca717011125f7b3fb4602b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 08:30:22 -0800 Subject: [PATCH 70/83] Remove unneeded attachToDom() call --- src/packages/command-logger/spec/command-logger-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index c7164454a..1eaee9318 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -9,7 +9,6 @@ describe "CommandLogger", -> atom.loadPackage 'command-logger' editor = rootView.getActiveEditor() commandLogger = CommandLogger.instance - rootView.attachToDom() afterEach -> rootView.deactivate() From 618d9f57486233ad773b431d4f56566648bde837 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 08:53:07 -0800 Subject: [PATCH 71/83] Replace fuzzy-finder with fuzzyFinder in spec config key path --- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index c6f655bfe..77bcd4ab4 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -251,28 +251,17 @@ describe 'FuzzyFinder', -> expect(rootView.project.getFilePaths).toHaveBeenCalled() describe "path ignoring", -> - it "ignores paths that match entries in config.fuzzy-finder.ignoredNames", -> + it "ignores paths that match entries in config.fuzzyFinder.ignoredNames", -> spyOn(rootView.project, "getFilePaths").andCallThrough() - config.set("fuzzy-finder.ignoredNames", ["tree-view"]) + config.set("fuzzyFinder.ignoredNames", ["tree-view.js"]) rootView.trigger 'fuzzy-finder:toggle-file-finder' finder.maxItems = Infinity - finder.miniEditor.setText("file1") waitsFor -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).toHaveBeenCalled() - rootView.project.getFilePaths.reset() - $(window).trigger 'focus' - rootView.trigger 'fuzzy-finder:toggle-file-finder' - rootView.trigger 'fuzzy-finder:toggle-file-finder' - - waitsFor -> - finder.list.children('li').length > 0 - - runs -> - expect(rootView.project.getFilePaths).toHaveBeenCalled() + expect(finder.list.find("li:contains(tree-view.js)")).not.toExist() describe "opening a path into a split", -> beforeEach -> From 7939b52da0724b8e934bf1cb546ab9d0993eda40 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 08:59:39 -0800 Subject: [PATCH 72/83] Marks paths for reload on focus and config events Show the last loaded paths and load the latest in the background when the FuzzyFinder is opened after a config or focus event has been fired. Previously the paths were completely cleared and the indexing message was displayed while the latest paths were loaded. --- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 5 ----- src/packages/fuzzy-finder/src/fuzzy-finder.coffee | 10 +++++++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 77bcd4ab4..f11ecda55 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -243,11 +243,6 @@ describe 'FuzzyFinder', -> $(window).trigger 'focus' rootView.trigger 'fuzzy-finder:toggle-file-finder' rootView.trigger 'fuzzy-finder:toggle-file-finder' - - waitsFor -> - finder.list.children('li').length > 0 - - runs -> expect(rootView.project.getFilePaths).toHaveBeenCalled() describe "path ignoring", -> diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee index 1e2566c15..44dc4441e 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee @@ -17,12 +17,13 @@ class FuzzyFinder extends SelectList allowActiveEditorChange: null maxItems: 10 projectPaths: null + reloadProjectPaths: true initialize: (@rootView) -> super - @subscribe $(window), 'focus', => @projectPaths = null - @observeConfig 'fuzzy-finder.ignoredNames', (ignoredNames) => - @projectPaths = null + + @subscribe $(window), 'focus', => @reloadProjectPaths = true + @observeConfig 'fuzzy-finder.ignoredNames', => @reloadProjectPaths = true @miniEditor.command 'editor:split-left', => @splitOpenPath (editor, session) -> editor.splitLeft(session) @@ -93,6 +94,8 @@ class FuzzyFinder extends SelectList @setArray(@projectPaths) else @setLoading("Indexing...") + + if @reloadProjectPaths @rootView.project.getFilePaths().done (paths) => ignoredNames = config.get("fuzzyFinder.ignoredNames") or [] ignoredNames = ignoredNames.concat(config.get("core.ignoredNames") or []) @@ -103,6 +106,7 @@ class FuzzyFinder extends SelectList return false if _.contains(ignoredNames, segment) return true + @reloadProjectPaths = false @setArray(@projectPaths) populateOpenBufferPaths: -> From 572b258547c9c0f4e109e15161e5510d415f2780 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 09:11:03 -0800 Subject: [PATCH 73/83] Only open paths that are files that exist The filesystem may have changed while the fuzzy finder is open or since the last time the paths were loaded so don't try to open paths unless they are files that currently exist when confirmed. --- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 12 ++++++++++++ src/packages/fuzzy-finder/src/fuzzy-finder.coffee | 8 ++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index f11ecda55..4453b2602 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -86,6 +86,18 @@ describe 'FuzzyFinder', -> expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() + describe "when the selected path isn't a file that exists", -> + it "leaves the the tree view open, doesn't open the path in the editor, and displays an error", -> + rootView.attachToDom() + path = rootView.getActiveEditor().getPath() + rootView.trigger 'fuzzy-finder:toggle-file-finder' + finder.confirmed('dir/this/is/not/a/file.txt') + expect(finder.hasParent()).toBeTruthy() + expect(rootView.getActiveEditor().getPath()).toBe path + expect(finder.find('.error').text().length).toBeGreaterThan 0 + advanceClock(2000) + expect(finder.find('.error').text().length).toBe 0 + describe "buffer-finder behavior", -> describe "toggling", -> describe "when the active editor contains edit sessions for buffers with paths", -> diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee index 44dc4441e..bda99cb1d 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee @@ -65,8 +65,12 @@ class FuzzyFinder extends SelectList confirmed : (path) -> return unless path.length - @cancel() - @openPath(path) + if fs.isFile(rootView.project.resolve(path)) + @cancel() + @openPath(path) + else + @setError('Selected path does not exist') + setTimeout(=> @setError('')), 2000 cancelled: -> @miniEditor.setText('') From 184f7d9f4541db3ee6e13e03de9abc17a66304fb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 09:22:41 -0800 Subject: [PATCH 74/83] :lipstick: --- src/packages/fuzzy-finder/src/fuzzy-finder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee index bda99cb1d..44a9aa17a 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee @@ -70,7 +70,7 @@ class FuzzyFinder extends SelectList @openPath(path) else @setError('Selected path does not exist') - setTimeout(=> @setError('')), 2000 + setTimeout((=> @setError()), 2000) cancelled: -> @miniEditor.setText('') From 7caf45dd25288c631f0440a0b0abe144f5446728 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 09:49:02 -0800 Subject: [PATCH 75/83] Make command heatmap take up all available space --- .../command-logger/src/command-logger.coffee | 48 +++++++++++-------- .../stylesheets/command-logger.css | 6 +-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/packages/command-logger/src/command-logger.coffee b/src/packages/command-logger/src/command-logger.coffee index 4011faef7..75b66cf29 100644 --- a/src/packages/command-logger/src/command-logger.coffee +++ b/src/packages/command-logger/src/command-logger.coffee @@ -86,6 +86,28 @@ class CommandLogger extends ScrollView @div style: "height:#{node.dy - 1}px;width:#{node.dx - 1}px", => @span node.name + updateCategoryHeader: (node) -> + @categoryHeader.text("#{node.name} Commands") + reduceRunCount = (previous, current) -> + if current.size? + previous + current.size + else if current.children?.length > 0 + current.children.reduce(reduceRunCount, previous) + else + previous + runCount = node.children.reduce(reduceRunCount, 0) + reduceCommandCount = (previous, current) -> + if current.children?.length > 0 + current.children.reduce(reduceCommandCount, previous) + else + previous + 1 + commandCount = node.children.reduce(reduceCommandCount, 0) + @categorySummary.text("#{_.pluralize(commandCount, 'command')}, #{_.pluralize(runCount, 'invocation')}") + + updateTreeMapSize: -> + @treeMap.width(@width() - 20) + @treeMap.height(@height() - @categoryHeader.outerHeight() - @categorySummary.outerHeight() - 20) + addTreeMap: -> root = name: 'All' @@ -94,33 +116,17 @@ class CommandLogger extends ScrollView @treeMap.empty() + @updateCategoryHeader(root) + @updateTreeMapSize() w = @treeMap.width() h = @treeMap.height() + x = d3.scale.linear().range([0, w]) y = d3.scale.linear().range([0, h]) color = d3.scale.category20() - updateCategoryHeader = (node) => - @categoryHeader.text("#{node.name} Commands") - reduceRunCount = (previous, current) -> - if current.size? - previous + current.size - else if current.children?.length > 0 - current.children.reduce(reduceRunCount, previous) - else - previous - runCount = node.children.reduce(reduceRunCount, 0) - reduceCommandCount = (previous, current) -> - if current.children?.length > 0 - current.children.reduce(reduceCommandCount, previous) - else - previous + 1 - commandCount = node.children.reduce(reduceCommandCount, 0) - @categorySummary.text("#{_.pluralize(commandCount, 'command')}, #{_.pluralize(runCount, 'invocation')}") - updateCategoryHeader(root) - - zoom = (d) -> - updateCategoryHeader(d) + zoom = (d) => + @updateCategoryHeader(d) kx = w / d.dx ky = h / d.dy x.domain([d.x, d.x + d.dx]) diff --git a/src/packages/command-logger/stylesheets/command-logger.css b/src/packages/command-logger/stylesheets/command-logger.css index b0be41559..5e9f34ffa 100644 --- a/src/packages/command-logger/stylesheets/command-logger.css +++ b/src/packages/command-logger/stylesheets/command-logger.css @@ -8,7 +8,8 @@ color: #eee; overflow: auto; z-index: 99; - padding: 20px; + padding-top: 10px; + padding-bottom: 10px; } .command-logger .category-header { @@ -25,9 +26,6 @@ .command-logger .tree-map { margin: auto; - position: relative; - width: 960px; - height: 700px; background-color: #efefef; border: 1px solid #999; } From 97b1bc5f09a4110465c8bbe24ce0562af934c719 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 09:54:00 -0800 Subject: [PATCH 76/83] editor:close-?-editors => editor:close-?-edit-sessions --- src/app/editor.coffee | 4 ++-- src/app/keymaps/editor.cson | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 5f5aaa831..720afce55 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -177,8 +177,8 @@ class Editor extends View 'editor:toggle-line-comments': @toggleLineCommentsInSelection 'editor:log-cursor-scope': @logCursorScope 'editor:checkout-head-revision': @checkoutHead - 'editor:close-other-editors': @destroyInactiveEditSessions - 'editor:close-all-editors': @destroyAllEditSessions + 'editor:close-other-edit-sessions': @destroyInactiveEditSessions + 'editor:close-all-edit-sessions': @destroyAllEditSessions 'editor:select-grammar': @selectGrammar documentation = {} diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 27ea35c55..376f822a2 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -32,6 +32,6 @@ 'meta-alt-p': 'editor:log-cursor-scope' 'meta-u': 'editor:upper-case' 'meta-U': 'editor:lower-case' - 'alt-meta-w': 'editor:close-other-editors' - 'meta-P': 'editor:close-all-editors' + 'alt-meta-w': 'editor:close-other-edit-sessions' + 'meta-P': 'editor:close-all-edit-sessions' 'meta-l': 'editor:select-grammar' From cecec376e3049abc4a0a22648334999c4988ae6e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:10:56 -0800 Subject: [PATCH 77/83] Use h1 for Packages and Features sections --- docs/features.md | 2 +- docs/packages/intro.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features.md b/docs/features.md index 304a5d833..ead022319 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1 +1 @@ -## Features +# Features diff --git a/docs/packages/intro.md b/docs/packages/intro.md index 30b318803..4f37c9446 100644 --- a/docs/packages/intro.md +++ b/docs/packages/intro.md @@ -1,4 +1,4 @@ -## Packages +# Packages ### Package Layout From bab6a12321658531b3008551d2b7913fbab9c610 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:15:55 -0800 Subject: [PATCH 78/83] Remove empty Packages section --- docs/configuring-and-extending.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/configuring-and-extending.md b/docs/configuring-and-extending.md index f868cdf96..f978f117d 100644 --- a/docs/configuring-and-extending.md +++ b/docs/configuring-and-extending.md @@ -157,9 +157,6 @@ directory, it will automatically be translated from TextMate's format to CSS so it works with Atom. There are a few slight differences between TextMate's semantics and those of stylesheets, but they should be negligible in practice. - -# Packages - ### Grammars ## TextMate Compatibility From 170b50ddf08d03c50ede610d33892da5b448811a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:25:41 -0800 Subject: [PATCH 79/83] Use search pattern with fewer matches to speed up spec --- .../command-panel/spec/command-panel-spec.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 83e69f075..500b401e9 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -290,22 +290,22 @@ describe "CommandPanel", -> rootView.attachToDom() editor.remove() rootView.trigger 'command-panel:toggle' - waitsForPromise -> commandPanel.execute('X x/a+/') + waitsForPromise -> commandPanel.execute('X x/quicksort/') it "displays and focuses the operation preview list", -> expect(commandPanel).toBeVisible() expect(commandPanel.previewList).toBeVisible() expect(commandPanel.previewList).toMatchSelector ':focus' - previewItem = commandPanel.previewList.find("li:contains(dir/a):first") - expect(previewItem.text()).toBe "dir/a" - expect(previewItem.next().find('.preview').text()).toBe "aaa bbb" - expect(previewItem.next().find('.preview > .match').text()).toBe "aaa" + previewItem = commandPanel.previewList.find("li:contains(sample.js):first") + expect(previewItem.text()).toBe "sample.js" + expect(previewItem.next().find('.preview').text()).toBe "var quicksort = function () {" + expect(previewItem.next().find('.preview > .match').text()).toBe "quicksort" rootView.trigger 'command-panel:toggle-preview' # ensure we can close panel without problems expect(commandPanel).toBeHidden() it "destroys previously previewed operations if there are any", -> - waitsForPromise -> commandPanel.execute('X x/b+/') + waitsForPromise -> commandPanel.execute('X x/pivot/') # there shouldn't be any dangling operations after this describe "if the command is malformed", -> From eaa164e1094ecc3c4291e145521bf64d3b79c817 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:37:33 -0800 Subject: [PATCH 80/83] Use subscribe for window focus event handler --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 99cf87eb6..f15e458c1 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -80,7 +80,7 @@ class RootView extends View handleEvents: -> @command 'toggle-dev-tools', => atom.toggleDevTools() @on 'focus', (e) => @handleFocus(e) - $(window).on 'focus', (e) => + @subscribe $(window), 'focus', (e) => @handleFocus(e) if document.activeElement is document.body @on 'root-view:active-path-changed', (e, path) => From a9506737673f7421e707c6f331cf6a8d0837be35 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:42:43 -0800 Subject: [PATCH 81/83] Remove duplicate simulateDomAttachment function --- spec/spec-helper.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 685004146..01736d4af 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -200,8 +200,5 @@ $.fn.textInput = (data) -> event = jQuery.event.fix(event) $(this).trigger(event) -$.fn.simulateDomAttachment = -> - $('').append(this) - unless fs.md5ForPath(require.resolve('fixtures/sample.js')) == "dd38087d0d7e3e4802a6d3f9b9745f2b" throw "Sample.js is modified" From 93fc1b6c4d25756d8668ce44397b3a0e3921ee2f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 10:51:01 -0800 Subject: [PATCH 82/83] Remove unneeded attachToDom() call in spec --- src/packages/markdown-preview/spec/markdown-preview-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 55a364261..aff38972b 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -9,7 +9,6 @@ describe "MarkdownPreview", -> rootView = new RootView(require.resolve('fixtures/markdown')) atom.loadPackage("markdown-preview") markdownPreview = MarkdownPreview.instance - rootView.attachToDom() afterEach -> rootView.deactivate() From 0123bc7b91064b0bf070cac588f196655ef355f1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 11 Jan 2013 11:30:54 -0800 Subject: [PATCH 83/83] Remove unneeded package name --- .../src/strip-trailing-whitespace.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee b/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee index 661a22986..bf01c6bbe 100644 --- a/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee +++ b/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee @@ -1,6 +1,4 @@ module.exports = - name: "strip trailing whitespace" - activate: (rootView) -> rootView.eachBuffer (buffer) => @stripTrailingWhitespaceBeforeSave(buffer)