Issue #29 Command and file autocomplete

This commit is contained in:
Stuart Quin 2016-08-03 18:41:13 +01:00
parent dfa44b5fa2
commit 7e1e03284a
4 changed files with 169 additions and 0 deletions

66
lib/autocomplete.coffee Normal file
View file

@ -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
)

View file

@ -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

View file

@ -109,6 +109,9 @@ class Ex
@registerAlias: (alias, name) =>
@singleton()[alias] = (args) => @singleton()[name](args)
@getCommands: () =>
Object.keys(@singleton())
quit: ->
atom.workspace.getActivePane().destroyActiveItem()

View file

@ -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)