diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee new file mode 100644 index 0000000..d33ffc3 --- /dev/null +++ b/lib/autocomplete.coffee @@ -0,0 +1,66 @@ +fs = require 'fs' +path = require 'path' +Ex = require './ex' + +module.exports = +class AutoComplete + constructor: (commands) -> + @commands = commands + @resetCompletion() + + resetCompletion: () -> + @autoCompleteIndex = 0 + @autoCompleteText = null + @completions = [] + + getAutocomplete: (text) -> + if !@autoCompleteText + @autoCompleteText = text + + parts = @autoCompleteText.split(' ') + cmd = parts[0] + + if parts.length > 1 + filePath = parts.slice(1).join(' ') + return @getCompletion(() => @getFilePathCompletion(cmd, filePath)) + else + return @getCompletion(() => @getCommandCompletion(cmd)) + + filterByPrefix: (commands, prefix) -> + commands.filter((f) => f.startsWith(prefix)) + + getCompletion: (completeFunc) -> + if @completions.length == 0 + @completions = completeFunc() + + if @completions.length + complete = @completions[@autoCompleteIndex % @completions.length] + @autoCompleteIndex++ + + # Only one result so lets return this directory + if complete.endsWith('/') && @completions.length == 1 + @resetCompletion() + + return complete + + getCommandCompletion: (command) -> + if @completions.length == 0 + return @filterByPrefix(@commands, command) + + getFilePathCompletion: (command, filePath) -> + if filePath.endsWith(path.sep) + basePath = path.dirname(filePath + '.') + baseName = '' + else + basePath = path.dirname(filePath) + baseName = path.basename(filePath) + + files = fs.readdirSync(basePath) + + return @filterByPrefix(files, baseName).map((f) => + filePath = path.join(basePath, f) + if fs.lstatSync(filePath).isDirectory() + return command + ' ' + filePath + path.sep + else + return command + ' ' + filePath + ) diff --git a/lib/ex-view-model.coffee b/lib/ex-view-model.coffee index 0c99be9..c8f1202 100644 --- a/lib/ex-view-model.coffee +++ b/lib/ex-view-model.coffee @@ -1,4 +1,6 @@ {ViewModel, Input} = require './view-model' +AutoComplete = require './autocomplete' +Ex = require './ex' module.exports = class ExViewModel extends ViewModel @@ -6,15 +8,31 @@ class ExViewModel extends ViewModel super(@exCommand, class: 'command') @historyIndex = -1 + @view.editorElement.addEventListener('keydown', @tabAutocomplete) atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx) atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx) + @autoComplete = new AutoComplete(Ex.getCommands()) + restoreHistory: (index) -> @view.editorElement.getModel().setText(@history(index).value) history: (index) -> @exState.getExHistoryItem(index) + tabAutocomplete: (event) => + if event.keyCode == 9 + event.stopPropagation() + event.preventDefault() + + completed = @autoComplete.getAutocomplete(@view.editorElement.getModel().getText()) + if completed + @view.editorElement.getModel().setText(completed) + + return false + else + @autoComplete.resetCompletion() + increaseHistoryEx: => if @history(@historyIndex + 1)? @historyIndex += 1 diff --git a/lib/ex.coffee b/lib/ex.coffee index c91beb7..0e46110 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -109,6 +109,9 @@ class Ex @registerAlias: (alias, name) => @singleton()[alias] = (args) => @singleton()[name](args) + @getCommands: () => + Object.keys(@singleton()) + quit: -> atom.workspace.getActivePane().destroyActiveItem() diff --git a/spec/autocomplete-spec.coffee b/spec/autocomplete-spec.coffee new file mode 100644 index 0000000..49b5a1e --- /dev/null +++ b/spec/autocomplete-spec.coffee @@ -0,0 +1,82 @@ +fs = require 'fs-plus' +path = require 'path' +os = require 'os' +uuid = require 'node-uuid' + +helpers = require './spec-helper' +AutoComplete = require '../lib/autocomplete' + +describe "autocomplete functionality", -> + beforeEach -> + @autoComplete = new AutoComplete(['taba', 'tabb', 'tabc']) + @testDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + @testFile1 = path.join(@testDir, "atom-ex-testfile-a.txt") + @testFile2 = path.join(@testDir, "atom-ex-testfile-b.txt") + + runs => + fs.makeTreeSync(@testDir) + fs.closeSync(fs.openSync(@testFile1, 'w')); + fs.closeSync(fs.openSync(@testFile2, 'w')); + spyOn(@autoComplete, 'resetCompletion').andCallThrough() + spyOn(@autoComplete, 'getFilePathCompletion').andCallThrough() + spyOn(@autoComplete, 'getCommandCompletion').andCallThrough() + + afterEach -> + fs.removeSync(@testDir) + + describe "autocomplete commands", -> + beforeEach -> + @completed = @autoComplete.getAutocomplete('tab') + + it "returns taba", -> + expect(@completed).toEqual('taba') + + it "calls command function", -> + expect(@autoComplete.getCommandCompletion.callCount).toBe(1) + + describe "autocomplete commands, then autoComplete again", -> + beforeEach -> + @completed = @autoComplete.getAutocomplete('tab') + @completed = @autoComplete.getAutocomplete('tab') + + it "returns tabb", -> + expect(@completed).toEqual('tabb') + + it "calls command function", -> + expect(@autoComplete.getCommandCompletion.callCount).toBe(1) + + describe "autocomplete directory", -> + beforeEach -> + filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + + it "returns testDir", -> + expected = 'tabe ' + @testDir + path.sep + expect(@completed).toEqual(expected) + + it "clears autocomplete", -> + expect(@autoComplete.resetCompletion.callCount).toBe(1) + + describe "autocomplete directory, then autocomplete again", -> + beforeEach -> + filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + @completed = @autoComplete.getAutocomplete(@completed) + + it "returns test file 1", -> + expect(@completed).toEqual('tabe ' + @testFile1) + + it "lists files twice", -> + expect(@autoComplete.getFilePathCompletion.callCount).toBe(2) + + describe "autocomplete full directory, then autocomplete again", -> + beforeEach -> + filePath = path.join(@testDir, 'a') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + @completed = @autoComplete.getAutocomplete(@completed) + + it "returns test file 2", -> + expect(@completed).toEqual('tabe ' + @testFile2) + + it "lists files once", -> + expect(@autoComplete.getFilePathCompletion.callCount).toBe(1)