diff --git a/README.md b/README.md index b3084de..7513c1d 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,11 @@ Use the service to register commands, from your own package, or straight from `i # in Atom's init.coffee atom.packages.onDidActivatePackage (pack) -> if pack.name == 'ex-mode' - Ex = pack.mainModule.provideEx() - Ex.registerCommand 'z', -> console.log("Zzzzzz...") + Ex = pack.mainModule.provideEx_0_30() + Ex.registerCommand + name: 'z' + priority: 1 + callback: -> console.log('zzzzzz') ``` See `lib/ex.coffee` for some examples commands. Contributions are very welcome! diff --git a/lib/command.coffee b/lib/command.coffee index 115b273..86a57cc 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -1,14 +1,44 @@ ExViewModel = require './ex-view-model' -Ex = require './ex' +ExCommands = require './ex-commands' Find = require './find' CommandError = require './command-error' +{getSearchTerm} = require './utils' + +cmp = (x, y) -> if x > y then 1 else if x < y then -1 else 0 class Command constructor: (@editor, @exState) -> @viewModel = new ExViewModel(@) + @vimState = @exState.globalExState.vim?.getEditorState(@editor) + + scanEditor: (term, position, reverse = false) -> + return if term is "" + + [rangesBefore, rangesAfter] = [[], []] + @editor.scan getSearchTerm(term), ({range}) -> + isBefore = if reverse + range.start.compare(position) < 0 + else + range.start.compare(position) <= 0 + + if isBefore + rangesBefore.push(range) + else + rangesAfter.push(range) + + if reverse + rangesAfter.concat(rangesBefore).reverse()[0] + else + rangesAfter.concat(rangesBefore)[0] + + checkForRepeatSearch: (term, reversed = false) -> + if term is '' or reversed and term is '?' or not reversed and term is '/' + @vimState.getSearchHistoryItem(0) + else + term parseAddr: (str, curPos) -> - if str is '.' + if str in ['.', ''] addr = curPos.row else if str is '$' # Lines are 0-indexed in Atom, but 1-indexed in vim. @@ -20,18 +50,22 @@ class Command else if str[0] is "'" # Parse Mark... unless @vimState? throw new CommandError("Couldn't get access to vim-mode.") - mark = @vimState.marks[str[1]] + mark = @vimState.getMark(str[1]) unless mark? throw new CommandError("Mark #{str} not set.") - addr = mark.bufferMarker.range.end.row - else if str[0] is "/" - addr = Find.findNextInBuffer(@editor.buffer, curPos, str[1...-1]) - unless addr? - throw new CommandError("Pattern not found: #{str[1...-1]}") - else if str[0] is "?" - addr = Find.findPreviousInBuffer(@editor.buffer, curPos, str[1...-1]) + addr = mark.row + else if (first = str[0]) in ['/', '?'] + reversed = first is '?' + str = @checkForRepeatSearch(str[1..], reversed) + throw new CommandError('No previous regular expression') if not str? + str = str[...-1] if str[str.length - 1] is first + @regex = str + lineRange = @editor.getLastCursor().getCurrentLineBufferRange() + pos = if reversed then lineRange.start else lineRange.end + addr = @scanEditor(str, pos, reversed) unless addr? throw new CommandError("Pattern not found: #{str[1...-1]}") + addr = addr.start.row return addr @@ -47,26 +81,25 @@ class Command else return -o - execute: (input) -> - @vimState = @exState.globalExState.vim?.getEditorState(@editor) + parseLine: (commandLine) -> # Command line parsing (mostly) following the rules at # http://pubs.opengroup.org/onlinepubs/9699919799/utilities # /ex.html#tag_20_40_13_03 + _commandLine = commandLine # Steps 1/2: Leading blanks and colons are ignored. - cl = input.characters - cl = cl.replace(/^(:|\s)*/, '') - return unless cl.length > 0 + commandLine = commandLine.replace(/^(:|\s)*/, '') + return unless commandLine.length > 0 # Step 3: If the first character is a ", ignore the rest of the line - if cl[0] is '"' + if commandLine[0] is '"' return # Step 4: Address parsing lastLine = @editor.getBuffer().lines.length - 1 - if cl[0] is '%' + if commandLine[0] is '%' range = [0, lastLine] - cl = cl[1..] + commandLine = commandLine[1..] else addrPattern = ///^ (?: # First address @@ -75,8 +108,8 @@ class Command \$| # Last line \d+| # n-th line '[\[\]<>'`"^.(){}a-zA-Z]| # Marks - /.*?[^\\]/| # Regex - \?.*?[^\\]\?| # Backwards search + /(?:.*?[^\\]|)(?:/|$)| # Regex + \?(?:.*?[^\\]|)(?:\?|$)| # Backwards search [+-]\d* # Current line +/- a number of lines )((?:\s*[+-]\d*)*) # Line offset )? @@ -86,14 +119,15 @@ class Command \$| \d+| '[\[\]<>'`"^.(){}a-zA-Z]| - /.*?[^\\]/| - \?.*?[^\\]\?| - [+-]\d* - )((?:\s*[+-]\d*)*) + /(?:.*?[^\\]|)(?:/|$)| + \?(?:.*?[^\\]|)(?:\?|$)| + [+-]\d*| + # Empty second address + )((?:\s*[+-]\d*)*)| )? /// - [match, addr1, off1, addr2, off2] = cl.match(addrPattern) + [match, addr1, off1, addr2, off2] = commandLine.match(addrPattern) curPos = @editor.getCursorBufferPosition() @@ -115,6 +149,9 @@ class Command if off2? address2 += @parseOffset(off2) + if @regex? + @vimState.pushSearchHistory(@regex) + if address2 < 0 or address2 > lastLine throw new CommandError('Invalid range') @@ -122,15 +159,15 @@ class Command throw new CommandError('Backwards range given') range = [address1, if address2? then address2 else address1] - cl = cl[match?.length..] + commandLine = commandLine[match?.length..] # Step 5: Leading blanks are ignored - cl = cl.trimLeft() + commandLine = commandLine.trimLeft() # Step 6a: If no command is specified, go to the last specified address - if cl.length is 0 + if commandLine.length is 0 @editor.setCursorBufferPosition([range[1], 0]) - return + return {range, command: undefined, args: undefined} # Ignore steps 6b and 6c since they only make sense for print commands and # print doesn't make sense @@ -139,31 +176,39 @@ class Command # Step 7b: :k is equal to :mark - only a-zA-Z is # in vim-mode for now - if cl.length is 2 and cl[0] is 'k' and /[a-z]/i.test(cl[1]) + if commandLine.length is 2 and commandLine[0] is 'k' \ + and /[a-z]/i.test(commandLine[1]) command = 'mark' - args = cl[1] - else if not /[a-z]/i.test(cl[0]) - command = cl[0] - args = cl[1..] + args = commandLine[1] + else if not /[a-z]/i.test(commandLine[0]) + command = commandLine[0] + args = commandLine[1..] else - [m, command, args] = cl.match(/^(\w+)(.*)/) + [m, command, args] = commandLine.match(/^(\w+)(.*)/) - # If the command matches an existing one exactly, execute that one - if (func = Ex.singleton()[command])? - func(range, args) - else - # Step 8: Match command against existing commands - matching = (name for name, val of Ex.singleton() when \ - name.indexOf(command) is 0) + commandLineRE = new RegExp("^" + command) + matching = [] - matching.sort() + for name in Object.keys(ExCommands.commands) + if commandLineRE.test(name) + command = ExCommands.commands[name] + if matching.length is 0 + matching = [command] + else + switch cmp(command.priority, matching[0].priority) + when 1 then matching = [command] + when 0 then matching.push(command) - command = matching[0] + command = matching.sort()[0] + unless command? + throw new CommandError("Not an editor command: #{_commandLine}") - func = Ex.singleton()[command] - if func? - func(range, args) - else - throw new CommandError("Not an editor command: #{input.characters}") + return {command: command.callback, range, args: args.trimLeft()} + + + execute: (input) -> + {command, range, args} = @parseLine(input.characters) + + command?({args, range, @editor, @exState, @vimState}) module.exports = Command diff --git a/lib/ex-commands.coffee b/lib/ex-commands.coffee new file mode 100644 index 0000000..ca282a9 --- /dev/null +++ b/lib/ex-commands.coffee @@ -0,0 +1,321 @@ +path = require 'path' +CommandError = require './command-error' +fs = require 'fs-plus' +VimOption = require './vim-option' +{getSearchTerm} = require './utils' + +trySave = (func) -> + deferred = Promise.defer() + + try + func() + deferred.resolve() + catch error + if error.message.endsWith('is a directory') + atom.notifications.addWarning("Unable to save file: #{error.message}") + else if error.path? + if error.code is 'EACCES' + atom.notifications + .addWarning("Unable to save file: Permission denied '#{error.path}'") + else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] + atom.notifications.addWarning("Unable to save file '#{error.path}'", + detail: error.message) + else if error.code is 'EROFS' + atom.notifications.addWarning( + "Unable to save file: Read-only file system '#{error.path}'") + else if (errorMatch = + /ENOTDIR, not a directory '([^']+)'/.exec(error.message)) + fileName = errorMatch[1] + atom.notifications.addWarning("Unable to save file: A directory in the "+ + "path '#{fileName}' could not be written to") + else + throw error + + deferred.promise + +saveAs = (filePath) -> + editor = atom.workspace.getActiveTextEditor() + fs.writeFileSync(filePath, editor.getText()) + +getFullPath = (filePath) -> + filePath = fs.normalize(filePath) + + if path.isAbsolute(filePath) + filePath + else if atom.project.getPaths().length == 0 + path.join(fs.normalize('~'), filePath) + else + path.join(atom.project.getPaths()[0], filePath) + +replaceGroups = (groups, string) -> + replaced = '' + escaped = false + while (char = string[0])? + string = string[1..] + if char is '\\' and not escaped + escaped = true + else if /\d/.test(char) and escaped + escaped = false + group = groups[parseInt(char)] + group ?= '' + replaced += group + else + escaped = false + replaced += char + + replaced + +module.exports = + class ExCommands + @commands = + 'quit': + priority: 1000 + callback: -> + atom.workspace.getActivePane().destroyActiveItem() + 'tabclose': + priority: 1000 + callback: (ev) => + @callCommand('quit', ev) + 'qall': + priority: 1000 + callback: -> + atom.close() + 'tabnext': + priority: 1000 + callback: -> + atom.workspace.getActivePane().activateNextItem() + 'tabprevious': + priority: 1000 + callback: -> + atom.workspace.getActivePane().activatePreviousItem() + 'write': + priority: 1001 + callback: ({editor, args}) -> + if args[0] is '!' + force = true + args = args[1..] + + filePath = args.trimLeft() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + + deferred = Promise.defer() + + if filePath.length isnt 0 + fullPath = getFullPath(filePath) + else if editor.getPath()? + trySave(-> editor.save()) + .then(deferred.resolve) + else + fullPath = atom.showSaveDialogSync() + + if fullPath? + if not force and fs.existsSync(fullPath) + throw new CommandError("File exists (add ! to override)") + trySave(-> saveAs(fullPath, editor)) + .then(deferred.resolve) + + deferred.promise + 'update': + priority: 1000 + callback: (ev) => + @callCommand('write', ev) + 'wall': + priority: 1000 + callback: -> + # FIXME: This is undocumented for quite a while now - not even + # deprecated. Should probably use PaneContainer::saveAll + atom.workspace.saveAll() + 'wq': + priority: 1000 + callback: (ev) => + @callCommand('write', ev).then => @callCommand('quit') + 'xit': + priority: 1000 + callback: (ev) => + @callCommand('wq', ev) + 'exit': + priority: 1000 + callback: (ev) => @callCommand('xit', ev) + 'xall': + priority: 1000 + callback: (ev) => + atom.workspace.saveAll() + @callCommand('qall', ev) + 'edit': + priority: 1001 + callback: ({args, editor}) -> + args = args.trim() + if args[0] is '!' + force = true + args = args[1..] + + if editor.isModified() and not force + throw new CommandError( + 'No write since last change (add ! to override)') + + filePath = args.trimLeft() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + + if filePath.length isnt 0 + fullPath = getFullPath(filePath) + if fullPath is editor.getPath() + editor.getBuffer().reload() + else + atom.workspace.open(fullPath) + else + if editor.getPath()? + editor.getBuffer().reload() + else + throw new CommandError('No file name') + 'tabedit': + priority: 1000 + callback: (ev) => + if ev.args.trim() is '' + @callCommand('tabnew', ev) + else + @callCommand('edit', ev) + 'tabnew': + priority: 1000 + callback: -> + atom.workspace.open() + 'split': + priority: 1000 + callback: ({args}) -> + filePath = args.trim() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + + pane = atom.workspace.getActivePane() + if filePath.length isnt 0 + # FIXME: This is horribly slow + atom.workspace.openURIInPane(getFullPath(filePath), pane.splitUp()) + else + pane.splitUp(copyActiveItem: true) + 'new': + priority: 1000 + callback: ({args}) -> + filePath = args.trim() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + filePath = undefined if filePath.length is 0 + # FIXME: This is horribly slow + atom.workspace.openURIInPane(filePath, + atom.workspace.getActivePane().splitUp()) + 'vsplit': + priority: 1000 + callback: ({args}) -> + filePath = args.trim() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + + pane = atom.workspace.getActivePane() + if filePath.length isnt 0 + # FIXME: This is horribly slow + atom.workspace.openURIInPane(getFullPath(filePath), + pane.splitLeft()) + else + pane.splitLeft(copyActiveItem: true) + 'vnew': + priority: 1000 + callback: ({args}) -> + filePath = args.trim() + if /[^\\] /.test(filePath) + throw new CommandError('Only one file name allowed') + filePath = filePath.replace(/\\ /g, ' ') + filePath = undefined if filePath.length is 0 + # FIXME: This is horribly slow + atom.workspace.openURIInPane(filePath, + atom.workspace.getActivePane().splitLeft()) + 'delete': + priority: 1000 + callback: ({range, editor}) -> + range = [[range[0], 0], [range[1] + 1, 0]] + editor.setTextInBufferRange(range, '') + 'substitute': + priority: 1001 + callback: ({range, args, editor, vimState}) -> + args_ = args.trimLeft() + delim = args_[0] + if /[a-z]/i.test(delim) + throw new CommandError( + "Regular expressions can't be delimited by letters") + if delim is '\\' + throw new CommandError( + "Regular expressions can't be delimited by \\") + args_ = args_[1..] + parsed = ['', '', ''] + parsing = 0 + escaped = false + while (char = args_[0])? + args_ = args_[1..] + if char is delim + if not escaped + parsing++ + if parsing > 2 + throw new CommandError('Trailing characters') + else + parsed[parsing] = parsed[parsing][...-1] + else if char is '\\' and not escaped + parsed[parsing] += char + escaped = true + else + escaped = false + parsed[parsing] += char + + [pattern, substition, flags] = parsed + if pattern is '' + pattern = vimState.getSearchHistoryItem(0) + if not pattern? + atom.beep() + throw new CommandError('No previous regular expression') + else + vimState.pushSearchHistory(pattern) + + try + flagsObj = {} + flags.split('').forEach((flag) -> flagsObj[flag] = true) + patternRE = getSearchTerm(pattern, flagsObj) + catch e + if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0 + throw new CommandError("Invalid flags: #{e.message[45..]}") + else if e.message.indexOf('Invalid regular expression: ') is 0 + throw new CommandError("Invalid RegEx: #{e.message[27..]}") + else + throw e + + editor.transact -> + for line in [range[0]..range[1]] + editor.scanInBufferRange( + patternRE, + [[line, 0], [line + 1, 0]], + ({match, replace}) -> + replace(replaceGroups(match[..], substition)) + ) + 'set': + priority: 1000 + callback: ({args}) -> + if args is '' + throw new CommandError('No option specified') + options = args.split(' ') + for option in options + if option[...2] is 'no' + VimOption.set(option[2..], false) + else if option[...3] is 'inv' + VimOption.inv(option[3..]) + else if option[-1..] is '!' + VimOption.inv(option[...-1]) + else + VimOption.set(option, true) + + @registerCommand: ({name, priority, callback}) => + @commands[name] = {priority, callback} + + @callCommand: (name, ev) => + @commands[name].callback(ev) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 1a4979d..c9edcc5 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -1,6 +1,6 @@ GlobalExState = require './global-ex-state' ExState = require './ex-state' -Ex = require './ex' +ExCommands = require './ex-commands' {Disposable, CompositeDisposable} = require 'event-kit' module.exports = ExMode = @@ -22,14 +22,18 @@ module.exports = ExMode = @exStates.set(editor, exState) - @disposables.add new Disposable => + @disposables.add new Disposable -> exState.destroy() deactivate: -> @disposables.dispose() - provideEx: -> - registerCommand: Ex.registerCommand.bind(Ex) + provideEx_0_20: -> + registerCommand: (name, callback) -> + ExCommands.registerCommand({name, callback, priority: 1}) + + provideEx_0_30: -> + registerCommand: ExCommands.registerCommand consumeVim: (vim) -> @vim = vim diff --git a/lib/ex.coffee b/lib/ex.coffee deleted file mode 100644 index e259c90..0000000 --- a/lib/ex.coffee +++ /dev/null @@ -1,287 +0,0 @@ -path = require 'path' -CommandError = require './command-error' -fs = require 'fs-plus' -VimOption = require './vim-option' - -trySave = (func) -> - deferred = Promise.defer() - - try - func() - deferred.resolve() - catch error - if error.message.endsWith('is a directory') - atom.notifications.addWarning("Unable to save file: #{error.message}") - else if error.path? - if error.code is 'EACCES' - atom.notifications - .addWarning("Unable to save file: Permission denied '#{error.path}'") - else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] - atom.notifications.addWarning("Unable to save file '#{error.path}'", - detail: error.message) - else if error.code is 'EROFS' - atom.notifications.addWarning( - "Unable to save file: Read-only file system '#{error.path}'") - else if (errorMatch = - /ENOTDIR, not a directory '([^']+)'/.exec(error.message)) - fileName = errorMatch[1] - atom.notifications.addWarning("Unable to save file: A directory in the "+ - "path '#{fileName}' could not be written to") - else - throw error - - deferred.promise - -saveAs = (filePath) -> - editor = atom.workspace.getActiveTextEditor() - fs.writeFileSync(filePath, editor.getText()) - -getFullPath = (filePath) -> - filePath = fs.normalize(filePath) - - if path.isAbsolute(filePath) - filePath - else if atom.project.getPaths().length == 0 - path.join(fs.normalize('~'), filePath) - else - path.join(atom.project.getPaths()[0], filePath) - -replaceGroups = (groups, string) -> - replaced = '' - escaped = false - while (char = string[0])? - string = string[1..] - if char is '\\' and not escaped - escaped = true - else if /\d/.test(char) and escaped - escaped = false - group = groups[parseInt(char)] - group ?= '' - replaced += group - else - escaped = false - replaced += char - - replaced - -class Ex - @singleton: => - @ex ||= new Ex - - @registerCommand: (name, func) => - @singleton()[name] = func - - quit: -> - atom.workspace.getActivePane().destroyActiveItem() - - q: => @quit() - - tabedit: (range, args) => - if args.trim() isnt '' - @edit(range, args) - else - @tabnew(range, args) - - tabe: (args...) => @tabedit(args...) - - tabnew: (range, args) => - if args.trim() is '' - atom.workspace.open() - else - @tabedit(range, args) - - tabclose: (args...) => @quit(args...) - - tabc: => @tabclose() - - tabnext: -> - pane = atom.workspace.getActivePane() - pane.activateNextItem() - - tabn: => @tabnext() - - tabprevious: -> - pane = atom.workspace.getActivePane() - pane.activatePreviousItem() - - tabp: => @tabprevious() - - edit: (range, filePath) -> - filePath = filePath.trim() - if filePath[0] is '!' - force = true - filePath = filePath[1..].trim() - else - force = false - - editor = atom.workspace.getActiveTextEditor() - if editor.isModified() and not force - throw new CommandError('No write since last change (add ! to override)') - if filePath.indexOf(' ') isnt -1 - throw new CommandError('Only one file name allowed') - - if filePath.length isnt 0 - fullPath = getFullPath(filePath) - if fullPath is editor.getPath() - editor.getBuffer().reload() - else - atom.workspace.open(fullPath) - else - if editor.getPath()? - editor.getBuffer().reload() - else - throw new CommandError('No file name') - - e: (args...) => @edit(args...) - - enew: -> - buffer = atom.workspace.getActiveTextEditor().buffer - buffer.setPath(undefined) - buffer.load() - - write: (range, filePath) -> - if filePath[0] is '!' - force = true - filePath = filePath[1..] - else - force = false - - filePath = filePath.trim() - if filePath.indexOf(' ') isnt -1 - throw new CommandError('Only one file name allowed') - - deferred = Promise.defer() - - editor = atom.workspace.getActiveTextEditor() - saved = false - if filePath.length isnt 0 - fullPath = getFullPath(filePath) - if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath) - # Use editor.save when no path is given or the path to the file is given - trySave(-> editor.save()).then(deferred.resolve) - saved = true - else if not fullPath? - fullPath = atom.showSaveDialogSync() - - if not saved and fullPath? - if not force and fs.existsSync(fullPath) - throw new CommandError("File exists (add ! to override)") - trySave(-> saveAs(fullPath)).then(deferred.resolve) - - deferred.promise - - w: (args...) => - @write(args...) - - wq: (args...) => - @write(args...).then => @quit() - - xit: (args...) => @wq(args...) - - wa: -> - atom.workspace.saveAll() - - split: (range, args) -> - args = args.trim() - filePaths = args.split(' ') - filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' - pane = atom.workspace.getActivePane() - if filePaths? and filePaths.length > 0 - newPane = pane.splitUp() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane - else - pane.splitUp(copyActiveItem: true) - - sp: (args...) => @split(args...) - - substitute: (range, args) -> - args = args.trimLeft() - delim = args[0] - if /[a-z]/i.test(delim) - throw new CommandError( - "Regular expressions can't be delimited by letters") - delimRE = new RegExp("[^\\\\]#{delim}") - spl = [] - args_ = args[1..] - while (i = args_.search(delimRE)) isnt -1 - spl.push args_[..i] - args_ = args_[i + 2..] - if args_.length is 0 and spl.length is 3 - throw new CommandError('Trailing characters') - else if args_.length isnt 0 - spl.push args_ - if spl.length > 3 - throw new CommandError('Trailing characters') - spl[1] ?= '' - spl[2] ?= '' - notDelimRE = new RegExp("\\\\#{delim}", 'g') - spl[0] = spl[0].replace(notDelimRE, delim) - spl[1] = spl[1].replace(notDelimRE, delim) - - try - pattern = new RegExp(spl[0], spl[2]) - catch e - if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0 - # vim only says 'Trailing characters', but let's be more descriptive - throw new CommandError("Invalid flags: #{e.message[45..]}") - else if e.message.indexOf('Invalid regular expression: ') is 0 - throw new CommandError("Invalid RegEx: #{e.message[27..]}") - else - throw e - - buffer = atom.workspace.getActiveTextEditor().buffer - atom.workspace.getActiveTextEditor().transact -> - for line in [range[0]..range[1]] - buffer.scanInRange(pattern, - [[line, 0], [line, buffer.lines[line].length]], - ({match, matchText, range, stop, replace}) -> - replace(replaceGroups(match[..], spl[1])) - ) - - s: (args...) => @substitute(args...) - - vsplit: (range, args) -> - args = args.trim() - filePaths = args.split(' ') - filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' - pane = atom.workspace.getActivePane() - if filePaths? and filePaths.length > 0 - newPane = pane.splitLeft() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane - else - pane.splitLeft(copyActiveItem: true) - - vsp: (args...) => @vsplit(args...) - - delete: (range) -> - range = [[range[0], 0], [range[1] + 1, 0]] - atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '') - - set: (range, args) -> - args = args.trim() - if args == "" - throw new CommandError("No option specified") - options = args.split(' ') - for option in options - do -> - if option.includes("=") - nameValPair = option.split("=") - if (nameValPair.length != 2) - throw new CommandError("Wrong option format. [name]=[value] format is expected") - optionName = nameValPair[0] - optionValue = nameValPair[1] - optionProcessor = VimOption.singleton()[optionName] - if not optionProcessor? - throw new CommandError("No such option: #{optionName}") - optionProcessor(optionValue) - else - optionProcessor = VimOption.singleton()[option] - if not optionProcessor? - throw new CommandError("No such option: #{option}") - optionProcessor() - -module.exports = Ex diff --git a/lib/utils.coffee b/lib/utils.coffee new file mode 100644 index 0000000..3967189 --- /dev/null +++ b/lib/utils.coffee @@ -0,0 +1,36 @@ +_ = require 'underscore-plus' + +module.exports = + getSearchTerm: (term, modifiers = {'g': true}) -> + escaped = false + hasc = false + hasC = false + term_ = term + term = '' + for char in term_ + if char is '\\' and not escaped + escaped = true + term += char + else + if char is 'c' and escaped + hasc = true + term = term[...-1] + else if char is 'C' and escaped + hasC = true + term = term[...-1] + else if char isnt '\\' + term += char + escaped = false + + if hasC + modifiers['i'] = false + if (not hasC and not term.match('[A-Z]') and \ + atom.config.get('vim-mode.useSmartcaseForSearch')) or hasc + modifiers['i'] = true + + modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('') + + try + new RegExp(term, modFlags) + catch + new RegExp(_.escapeRegExp(term), modFlags) diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee index 2ee056c..3f2ea4c 100644 --- a/lib/vim-option.coffee +++ b/lib/vim-option.coffee @@ -1,23 +1,19 @@ class VimOption - @singleton: => - @option ||= new VimOption + @options = + 'list': 'editor.showInvisibles' + 'nu': 'editor.showLineNumbers' + 'number': 'editor.showLineNumbers' - list: => - atom.config.set("editor.showInvisibles", true) + @registerOption: (vimName, atomName) -> + @options[vimName] = atomName - nolist: => - atom.config.set("editor.showInvisibles", false) + @set: (name, value) -> + atom.config.set(@options[name], value) - number: => - atom.config.set("editor.showLineNumbers", true) + @get: (name) -> + atom.config.get(@options[name]) - nu: => - @number() - - nonumber: => - atom.config.set("editor.showLineNumbers", false) - - nonu: => - @nonumber() + @inv: (name) -> + atom.config.set(@options[name], not atom.config.get(@options[name])) module.exports = VimOption diff --git a/package.json b/package.json index 5b986cb..f496e9c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "ex-mode": { "description": "Ex commands", "versions": { - "0.20.0": "provideEx" + "0.20.0": "provideEx_0_20", + "0.30.0": "provideEx_0_30" } } }, diff --git a/spec/command-parsing-spec.coffee b/spec/command-parsing-spec.coffee new file mode 100644 index 0000000..d14b0ad --- /dev/null +++ b/spec/command-parsing-spec.coffee @@ -0,0 +1,185 @@ +helpers = require './spec-helper' +Command = require '../lib/command' +ExCommands = require('../lib/ex-commands') + +describe "command parsing", -> + [editor, editorElement, vimState, exState, command, lines] = [] + beforeEach -> + vimMode = atom.packages.loadPackage('vim-mode') + exMode = atom.packages.loadPackage('ex-mode') + exMode.activate() + + waitsForPromise -> + vimMode.activate().then -> + helpers.activateExMode().then -> + helpers.getEditorElement (element) -> + editorElement = element + editor = editorElement.getModel() + atom.commands.dispatch(element, 'ex-mode:open') + atom.commands.dispatch(editor.normalModeInputView.editorElement, + 'core:cancel') + vimState = vimMode.mainModule.getEditorState(editor) + exState = exMode.mainModule.exStates.get(editor) + command = new Command(editor, exState) + vimState.activateNormalMode() + vimState.resetNormalMode() + editor.setText( + 'abc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc' + '\nabc\nabc\n') + lines = editor.getBuffer().getLines() + editor.setCursorBufferPosition([0, 0]) + + it "parses a simple command (e.g. `:quit`)", -> + expect(command.parseLine('quit')).toEqual + command: ExCommands.commands.quit.callback + args: '' + range: [0, 0] + + it "matches sub-commands (e.g. `:q`)", -> + expect(command.parseLine('q')).toEqual + command: ExCommands.commands.quit.callback + args: '' + range: [0, 0] + + it "uses the command with the highest priority if multiple match an input", -> + expect(command.parseLine('s').command) + .toEqual(ExCommands.commands.substitute.callback) + + it "ignores leading blanks and spaces", -> + expect(command.parseLine(':::: :::: : : : ')).toBeUndefined + expect(command.parseLine(':: :::::: :quit')).toEqual + command: ExCommands.commands.quit.callback + args: '' + range: [0, 0] + expect(atom.notifications.notifications.length).toBe(0) + + it 'ignores the line if it starts with a "', -> + expect(command.parseLine('"quit')).toBe(undefined) + expect(atom.notifications.notifications.length).toBe(0) + + describe "address parsing", -> + describe "with only one address", -> + it "parses . as an address", -> + expect(command.parseLine('.').range).toEqual([0, 0]) + editor.setCursorBufferPosition([2, 0]) + expect(command.parseLine('.').range).toEqual([2, 2]) + + it "parses $ as an address", -> + expect(command.parseLine('$').range) + .toEqual([lines.length - 1, lines.length - 1]) + + it "parses a number as an address", -> + expect(command.parseLine('3').range).toEqual([2, 2]) + expect(command.parseLine('7').range).toEqual([6, 6]) + + it "parses 'a as an address", -> + vimState.setMark('a', [3, 1]) + expect(command.parseLine("'a").range).toEqual([3, 3]) + + it "throws an error if the mark is not set", -> + vimState.marks.a = undefined + expect(-> command.parseLine("'a")).toThrow() + + it "parses /a and ?a as addresses", -> + expect(command.parseLine('/abc').range).toEqual([1, 1]) + editor.setCursorBufferPosition([1, 0]) + expect(command.parseLine('?abc').range).toEqual([0, 0]) + editor.setCursorBufferPosition([0, 0]) + expect(command.parseLine('/bc').range).toEqual([1, 1]) + + it "integrates the search history for :/", -> + vimState.pushSearchHistory('abc') + expect(command.parseLine('//').range).toEqual([1, 1]) + command.parseLine('/ab/,/bc/+2') + expect(vimState.getSearchHistoryItem(0)).toEqual('bc') + + describe "case sensitivity for search patterns", -> + beforeEach -> + editor.setText('abca\nABC\ndefdDEF\nabcaABC') + + describe "respects the smartcase setting", -> + describe "with smartcasse off", -> + beforeEach -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + editor.setCursorBufferPosition([0, 0]) + + it "uses case sensitive search if pattern is lowercase", -> + expect(command.parseLine('/abc').range).toEqual([3, 3]) + + it "uses case sensitive search if the pattern is uppercase", -> + expect(command.parseLine('/ABC').range).toEqual([1, 1]) + + describe "with smartcase on", -> + beforeEach -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + + it "uses case insensitive search if the pattern is lowercase", -> + editor.setCursorBufferPosition([0, 0]) + expect(command.parseLine('/abc').range).toEqual([1, 1]) + + it "uses case sensitive search if the pattern is uppercase", -> + editor.setCursorBufferPosition([3, 3]) + expect(command.parseLine('/ABC').range).toEqual([1, 1]) + + describe "\\c and \\C", -> + describe "only \\c in the pattern", -> + beforeEach -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + editor.setCursorBufferPosition([0, 0]) + + it "uses case insensitive search if smartcase is off", -> + expect(command.parseLine('/abc\\c').range).toEqual([1, 1]) + + it "doesn't matter where it is", -> + expect(command.parseLine('/ab\\cc').range).toEqual([1, 1]) + + describe "only \\C in the pattern with smartcase on", -> + beforeEach -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + editor.setCursorBufferPosition([0, 0]) + + it "uses case sensitive search if the pattern is lowercase", -> + expect(command.parseLine('/abc\\C').range).toEqual([3, 3]) + + it "doesn't matter where it is", -> + expect(command.parseLine('/ab\\Cc').range).toEqual([3, 3]) + + describe "with \\c and \\C in the pattern", -> + beforeEach -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + editor.setCursorBufferPosition([0, 0]) + + it "uses case insensitive search if \\C comes first", -> + expect(command.parseLine('/a\\Cb\\cc').range).toEqual([1, 1]) + + it "uses case insensitive search if \\c comes first", -> + expect(command.parseLine('/a\\cb\\Cc').range).toEqual([1, 1]) + + describe "with two addresses", -> + it "parses both", -> + expect(command.parseLine('5,10').range).toEqual([4, 9]) + + it "throws an error if it is in reverse order", -> + expect(-> command.parseLine('10,5').range).toThrow() + + it "uses the current line as second address if empty", -> + editor.setCursorBufferPosition([3, 0]) + expect(command.parseLine('-2,').range).toEqual([1, 3]) + + it "parses a command with a range and no arguments", -> + expect(command.parseLine('2,/abc/+4delete')).toEqual + command: ExCommands.commands.delete.callback + args: '' + range: [1, 5] + + it "parses a command with no range and arguments", -> + expect(command.parseLine('edit edit-test test-2')).toEqual + command: ExCommands.commands.edit.callback + args: 'edit-test test-2' + range: [0, 0] + + it "parses a command with range and arguments", -> + expect(command.parseLine('3,5+2s/abc/def/gi')).toEqual + command: ExCommands.commands.substitute.callback + args: '/abc/def/gi' + range: [2, 6] diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 803aa57..b3650fe 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -3,8 +3,7 @@ path = require 'path' os = require 'os' uuid = require 'node-uuid' helpers = require './spec-helper' - -Ex = require('../lib/ex').singleton() +ExCommands = require('../lib/ex-commands') describe "the commands", -> [editor, editorElement, vimState, exState, dir, dir2] = [] @@ -177,11 +176,25 @@ describe "the commands", -> describe ":tabclose", -> it "acts as an alias to :quit", -> - spyOn(Ex, 'tabclose').andCallThrough() - spyOn(Ex, 'quit').andCallThrough() + spyOn(ExCommands.commands.tabclose, 'callback').andCallThrough() + spyOn(ExCommands.commands.quit, 'callback').andCallThrough() keydown(':') submitNormalModeInputText('tabclose') - expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...) + expect(ExCommands.commands.quit.callback) + .toHaveBeenCalledWith(ExCommands.commands.tabclose.callback + .calls[0].args[0]) + + describe ":qall", -> + beforeEach -> + waitsForPromise -> + atom.workspace.open().then -> atom.workspace.open() + .then -> atom.workspace.open() + + it "closes the window", -> + spyOn(atom, 'close') + keydown(':') + submitNormalModeInputText('qall') + expect(atom.close).toHaveBeenCalled() describe ":tabnext", -> pane = null @@ -223,45 +236,81 @@ describe "the commands", -> submitNormalModeInputText('tabprevious') expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1) + describe ":update", -> + it "acts as an alias to :write", -> + spyOn(ExCommands.commands.update, 'callback') + .andCallThrough() + spyOn(ExCommands.commands.write, 'callback') + keydown(':') + submitNormalModeInputText('update') + expect(ExCommands.commands.write.callback).toHaveBeenCalledWith( + ExCommands.commands.update.callback.calls[0].args[0]) + + describe ":wall", -> + it "saves all open files", -> + spyOn(atom.workspace, 'saveAll') + keydown(':') + submitNormalModeInputText('wall') + expect(atom.workspace.saveAll).toHaveBeenCalled() + describe ":wq", -> beforeEach -> - spyOn(Ex, 'write').andCallThrough() - spyOn(Ex, 'quit') + spyOn(ExCommands.commands.write, 'callback').andCallThrough() + spyOn(ExCommands.commands.quit, 'callback') it "writes the file, then quits", -> spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1')) keydown(':') submitNormalModeInputText('wq') - expect(Ex.write).toHaveBeenCalled() + expect(ExCommands.commands.write.callback).toHaveBeenCalled() # Since `:wq` only calls `:quit` after `:write` is finished, we need to # wait a bit for the `:quit` call to occur - waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) + waitsFor((-> ExCommands.commands.quit.callback.wasCalled), + "the :quit command to be called", 100) it "doesn't quit when the file is new and no path is specified in the save dialog", -> spyOn(atom, 'showSaveDialogSync').andReturn(undefined) keydown(':') submitNormalModeInputText('wq') - expect(Ex.write).toHaveBeenCalled() + expect(ExCommands.commands.write.callback).toHaveBeenCalled() wasNotCalled = false # FIXME: This seems dangerous, but setTimeout somehow doesn't work. setImmediate((-> - wasNotCalled = not Ex.quit.wasCalled)) + wasNotCalled = not ExCommands.commands.quit.callback.wasCalled)) waitsFor((-> wasNotCalled), 100) it "passes the file name", -> keydown(':') submitNormalModeInputText('wq wq-2') - expect(Ex.write) + expect(ExCommands.commands.write.callback) .toHaveBeenCalled() - expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2') - waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) + expect(ExCommands.commands.write.callback.calls[0].args[0].args) + .toEqual('wq-2') + waitsFor((-> ExCommands.commands.quit.callback.wasCalled), + "the :quit command to be called", 100) describe ":xit", -> it "acts as an alias to :wq", -> - spyOn(Ex, 'wq') + spyOn(ExCommands.commands.wq, 'callback') keydown(':') submitNormalModeInputText('xit') - expect(Ex.wq).toHaveBeenCalled() + expect(ExCommands.commands.wq.callback).toHaveBeenCalled() + + describe ":exit", -> + it "is an alias to :xit", -> + spyOn(ExCommands.commands.xit, 'callback') + keydown(':') + submitNormalModeInputText('exit') + expect(ExCommands.commands.xit.callback).toHaveBeenCalled() + + describe ":xall", -> + it "saves all open files and closes the window", -> + spyOn(atom.workspace, 'saveAll') + spyOn(atom, 'close') + keydown(':') + submitNormalModeInputText('xall') + expect(atom.workspace.saveAll).toHaveBeenCalled() + expect(atom.close).toHaveBeenCalled() describe ":edit", -> describe "without a file name", -> @@ -339,19 +388,20 @@ describe "the commands", -> describe ":tabedit", -> it "acts as an alias to :edit if supplied with a path", -> - spyOn(Ex, 'tabedit').andCallThrough() - spyOn(Ex, 'edit') + spyOn(ExCommands.commands.tabedit, 'callback').andCallThrough() + spyOn(ExCommands.commands.edit, 'callback') keydown(':') submitNormalModeInputText('tabedit tabedit-test') - expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) + expect(ExCommands.commands.edit.callback).toHaveBeenCalledWith( + ExCommands.commands.tabedit.callback.calls[0].args...) it "acts as an alias to :tabnew if not supplied with a path", -> - spyOn(Ex, 'tabedit').andCallThrough() - spyOn(Ex, 'tabnew') + spyOn(ExCommands.commands.tabedit, 'callback').andCallThrough() + spyOn(ExCommands.commands.tabnew, 'callback') keydown(':') submitNormalModeInputText('tabedit ') - expect(Ex.tabnew) - .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) + expect(ExCommands.commands.tabnew.callback).toHaveBeenCalledWith( + ExCommands.commands.tabedit.callback.calls[0].args...) describe ":tabnew", -> it "opens a new tab", -> @@ -372,6 +422,15 @@ describe "the commands", -> # FIXME: Should test whether the new pane contains a TextEditor # pointing to the same path + describe ":new", -> + it "splits a new file upwards", -> + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitUp').andCallThrough() + keydown(':') + submitNormalModeInputText('new') + expect(pane.splitUp).toHaveBeenCalled() + # FIXME: Should test whether the new pane contains an empty file + describe ":vsplit", -> it "splits the current file to the left", -> pane = atom.workspace.getActivePane() @@ -384,6 +443,15 @@ describe "the commands", -> # FIXME: Should test whether the new pane contains a TextEditor # pointing to the same path + describe ":vnew", -> + it "splits a new file to the left", -> + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitLeft').andCallThrough() + keydown(':') + submitNormalModeInputText('vnew') + expect(pane.splitLeft).toHaveBeenCalled() + # FIXME: Should test whether the new pane contains an empty file + describe ":delete", -> beforeEach -> editor.setText('abc\ndef\nghi\njkl') @@ -449,13 +517,89 @@ describe "the commands", -> submitNormalModeInputText(':%substitute/abc/ghi/ig') expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') - it "can't be delimited by letters", -> + it "can't be delimited by letters or \\", -> keydown(':') submitNormalModeInputText(':substitute nanxngi') expect(atom.notifications.notifications[0].message).toEqual( "Command error: Regular expressions can't be delimited by letters") expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':substitute\\a\\x\\gi') + expect(atom.notifications.notifications[1].message).toEqual( + "Command error: Regular expressions can't be delimited by \\") + expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + + describe "case sensitivity", -> + describe "respects the smartcase setting", -> + beforeEach -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + + it "uses case sensitive search if smartcase is off and the pattern is lowercase", -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + keydown(':') + submitNormalModeInputText(':substitute/abc/ghi/g') + expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') + + it "uses case sensitive search if smartcase is off and the pattern is uppercase", -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + keydown(':') + submitNormalModeInputText(':substitute/ABC/ghi/g') + expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC') + + it "uses case insensitive search if smartcase is on and the pattern is lowercase", -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + atom.config.set('vim-mode.useSmartcaseForSearch', true) + keydown(':') + submitNormalModeInputText(':substitute/abc/ghi/g') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') + + it "uses case sensitive search if smartcase is on and the pattern is uppercase", -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + keydown(':') + submitNormalModeInputText(':substitute/ABC/ghi/g') + expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC') + + describe "\\c and \\C in the pattern", -> + beforeEach -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + + it "uses case insensitive search if smartcase is off and \c is in the pattern", -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + keydown(':') + submitNormalModeInputText(':substitute/abc\\c/ghi/g') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') + + it "doesn't matter where in the pattern \\c is", -> + atom.config.set('vim-mode.useSmartcaseForSearch', false) + keydown(':') + submitNormalModeInputText(':substitute/a\\cbc/ghi/g') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') + + it "uses case sensitive search if smartcase is on, \\C is in the pattern and the pattern is lowercase", -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + keydown(':') + submitNormalModeInputText(':substitute/a\\Cbc/ghi/g') + expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') + + it "overrides \\C with \\c if \\C comes first", -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + keydown(':') + submitNormalModeInputText(':substitute/a\\Cb\\cc/ghi/g') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') + + it "overrides \\C with \\c if \\c comes first", -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + keydown(':') + submitNormalModeInputText(':substitute/a\\cb\\Cc/ghi/g') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC') + + it "overrides an appended /i flag with \\C", -> + atom.config.set('vim-mode.useSmartcaseForSearch', true) + keydown(':') + submitNormalModeInputText(':substitute/ab\\Cc/ghi/gi') + expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') + describe "capturing groups", -> beforeEach -> editor.setText('abcaABC\ndefdDEF\nabcaABC') @@ -478,7 +622,7 @@ describe "the commands", -> describe ":set", -> it "throws an error without a specified option", -> keydown(':') - submitNormalModeInputText(':set') + submitNormalModeInputText('set') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: No option specified') @@ -486,33 +630,47 @@ describe "the commands", -> atom.config.set('editor.showInvisibles', false) atom.config.set('editor.showLineNumbers', false) keydown(':') - submitNormalModeInputText(':set list number') + submitNormalModeInputText('set list number') expect(atom.config.get('editor.showInvisibles')).toBe(true) expect(atom.config.get('editor.showLineNumbers')).toBe(true) - describe "the options", -> - beforeEach -> - atom.config.set('editor.showInvisibles', false) - atom.config.set('editor.showLineNumbers', false) + it "sets options to false with no{option}", -> + atom.config.set('editor.showInvisibles', true) + keydown(':') + submitNormalModeInputText('set nolist') + expect(atom.config.get('editor.showInvisibles')).toBe(false) - it "sets (no)list", -> + it "inverts options with inv{option}", -> + atom.config.set('editor.showInvisibles', true) + keydown(':') + submitNormalModeInputText('set invlist') + expect(atom.config.get('editor.showInvisibles')).toBe(false) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText('set invlist') + expect(atom.config.get('editor.showInvisibles')).toBe(true) + + it "inverts options with {option}!", -> + atom.config.set('editor.showInvisibles', true) + keydown(':') + submitNormalModeInputText('set list!') + expect(atom.config.get('editor.showInvisibles')).toBe(false) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText('set list!') + expect(atom.config.get('editor.showInvisibles')).toBe(true) + + describe "the options", -> + it "sets list", -> + atom.config.set('editor.showInvisibles', false) keydown(':') submitNormalModeInputText(':set list') expect(atom.config.get('editor.showInvisibles')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nolist') - expect(atom.config.get('editor.showInvisibles')).toBe(false) - it "sets (no)nu(mber)", -> + it "sets nu[mber]", -> + atom.config.set('editor.showLineNumbers', false) keydown(':') submitNormalModeInputText(':set nu') expect(atom.config.get('editor.showLineNumbers')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nonu') - expect(atom.config.get('editor.showLineNumbers')).toBe(false) + atom.config.set('editor.showLineNumbers', false) atom.commands.dispatch(editorElement, 'ex-mode:open') submitNormalModeInputText(':set number') expect(atom.config.get('editor.showLineNumbers')).toBe(true) - atom.commands.dispatch(editorElement, 'ex-mode:open') - submitNormalModeInputText(':set nonumber') - expect(atom.config.get('editor.showLineNumbers')).toBe(false)