diff options
| author | Ben Beltran <ben@nsovocal.com> | 2015-07-10 11:12:25 -0500 |
|---|---|---|
| committer | Ben Beltran <ben@nsovocal.com> | 2015-07-10 11:12:25 -0500 |
| commit | 24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (patch) | |
| tree | ded312222bb108923da1820ba40b04d710d20e5b /atom/packages/ex-mode/lib | |
| parent | eb786e82d170e2abc351a432ade616d6ecdeeb6b (diff) | |
Adds atom
Diffstat (limited to 'atom/packages/ex-mode/lib')
| -rw-r--r-- | atom/packages/ex-mode/lib/command-error.coffee | 5 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/command.coffee | 169 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee | 64 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/ex-mode.coffee | 36 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/ex-state.coffee | 59 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/ex-view-model.coffee | 35 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/ex.coffee | 231 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/find.coffee | 26 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/global-ex-state.coffee | 5 | ||||
| -rw-r--r-- | atom/packages/ex-mode/lib/view-model.coffee | 26 |
10 files changed, 656 insertions, 0 deletions
diff --git a/atom/packages/ex-mode/lib/command-error.coffee b/atom/packages/ex-mode/lib/command-error.coffee new file mode 100644 index 0000000..c9861e9 --- /dev/null +++ b/atom/packages/ex-mode/lib/command-error.coffee @@ -0,0 +1,5 @@ +class CommandError + constructor: (@message) -> + @name = 'Command Error' + +module.exports = CommandError diff --git a/atom/packages/ex-mode/lib/command.coffee b/atom/packages/ex-mode/lib/command.coffee new file mode 100644 index 0000000..115b273 --- /dev/null +++ b/atom/packages/ex-mode/lib/command.coffee @@ -0,0 +1,169 @@ +ExViewModel = require './ex-view-model' +Ex = require './ex' +Find = require './find' +CommandError = require './command-error' + +class Command + constructor: (@editor, @exState) -> + @viewModel = new ExViewModel(@) + + parseAddr: (str, curPos) -> + if str is '.' + addr = curPos.row + else if str is '$' + # Lines are 0-indexed in Atom, but 1-indexed in vim. + addr = @editor.getBuffer().lines.length - 1 + else if str[0] in ["+", "-"] + addr = curPos.row + @parseOffset(str) + else if not isNaN(str) + addr = parseInt(str) - 1 + else if str[0] is "'" # Parse Mark... + unless @vimState? + throw new CommandError("Couldn't get access to vim-mode.") + mark = @vimState.marks[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]) + unless addr? + throw new CommandError("Pattern not found: #{str[1...-1]}") + + return addr + + parseOffset: (str) -> + if str.length is 0 + return 0 + if str.length is 1 + o = 1 + else + o = parseInt(str[1..]) + if str[0] is '+' + return o + else + return -o + + execute: (input) -> + @vimState = @exState.globalExState.vim?.getEditorState(@editor) + # Command line parsing (mostly) following the rules at + # http://pubs.opengroup.org/onlinepubs/9699919799/utilities + # /ex.html#tag_20_40_13_03 + + # Steps 1/2: Leading blanks and colons are ignored. + cl = input.characters + cl = cl.replace(/^(:|\s)*/, '') + return unless cl.length > 0 + + # Step 3: If the first character is a ", ignore the rest of the line + if cl[0] is '"' + return + + # Step 4: Address parsing + lastLine = @editor.getBuffer().lines.length - 1 + if cl[0] is '%' + range = [0, lastLine] + cl = cl[1..] + else + addrPattern = ///^ + (?: # First address + ( + \.| # Current line + \$| # Last line + \d+| # n-th line + '[\[\]<>'`"^.(){}a-zA-Z]| # Marks + /.*?[^\\]/| # Regex + \?.*?[^\\]\?| # Backwards search + [+-]\d* # Current line +/- a number of lines + )((?:\s*[+-]\d*)*) # Line offset + )? + (?:, # Second address + ( # Same as first address + \.| + \$| + \d+| + '[\[\]<>'`"^.(){}a-zA-Z]| + /.*?[^\\]/| + \?.*?[^\\]\?| + [+-]\d* + )((?:\s*[+-]\d*)*) + )? + /// + + [match, addr1, off1, addr2, off2] = cl.match(addrPattern) + + curPos = @editor.getCursorBufferPosition() + + if addr1? + address1 = @parseAddr(addr1, curPos) + else + # If no addr1 is given (,+3), assume it is '.' + address1 = curPos.row + if off1? + address1 += @parseOffset(off1) + + address1 = 0 if address1 is -1 + + if address1 < 0 or address1 > lastLine + throw new CommandError('Invalid range') + + if addr2? + address2 = @parseAddr(addr2, curPos) + if off2? + address2 += @parseOffset(off2) + + if address2 < 0 or address2 > lastLine + throw new CommandError('Invalid range') + + if address2 < address1 + throw new CommandError('Backwards range given') + + range = [address1, if address2? then address2 else address1] + cl = cl[match?.length..] + + # Step 5: Leading blanks are ignored + cl = cl.trimLeft() + + # Step 6a: If no command is specified, go to the last specified address + if cl.length is 0 + @editor.setCursorBufferPosition([range[1], 0]) + return + + # Ignore steps 6b and 6c since they only make sense for print commands and + # print doesn't make sense + + # Ignore step 7a since flags are only useful for print + + # Step 7b: :k<valid mark> is equal to :mark <valid 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]) + command = 'mark' + args = cl[1] + else if not /[a-z]/i.test(cl[0]) + command = cl[0] + args = cl[1..] + else + [m, command, args] = cl.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) + + matching.sort() + + command = matching[0] + + func = Ex.singleton()[command] + if func? + func(range, args) + else + throw new CommandError("Not an editor command: #{input.characters}") + +module.exports = Command diff --git a/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee b/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee new file mode 100644 index 0000000..91e0eb2 --- /dev/null +++ b/atom/packages/ex-mode/lib/ex-command-mode-input-element.coffee @@ -0,0 +1,64 @@ +class ExCommandModeInputElement extends HTMLDivElement + createdCallback: -> + @className = "command-mode-input" + + @editorContainer = document.createElement("div") + @editorContainer.className = "editor-container" + + @appendChild(@editorContainer) + + initialize: (@viewModel, opts = {}) -> + if opts.class? + @editorContainer.classList.add(opts.class) + + if opts.hidden + @editorContainer.style.height = "0px" + + @editorElement = document.createElement "atom-text-editor" + @editorElement.classList.add('editor') + @editorElement.getModel().setMini(true) + @editorElement.setAttribute('mini', '') + @editorContainer.appendChild(@editorElement) + + @singleChar = opts.singleChar + @defaultText = opts.defaultText ? '' + + @panel = atom.workspace.addBottomPanel(item: this, priority: 100) + + @focus() + @handleEvents() + + this + + handleEvents: -> + if @singleChar? + @editorElement.getModel().getBuffer().onDidChange (e) => + @confirm() if e.newText + else + atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this)) + + atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this)) + atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this)) + atom.commands.add(@editorElement, 'blur', @cancel.bind(this)) + + confirm: -> + @value = @editorElement.getModel().getText() or @defaultText + @viewModel.confirm(this) + @removePanel() + + focus: -> + @editorElement.focus() + + cancel: (e) -> + @viewModel.cancel(this) + @removePanel() + + removePanel: -> + atom.workspace.getActivePane().activate() + @panel.destroy() + +module.exports = +document.registerElement("ex-command-mode-input" + extends: "div", + prototype: ExCommandModeInputElement.prototype +) diff --git a/atom/packages/ex-mode/lib/ex-mode.coffee b/atom/packages/ex-mode/lib/ex-mode.coffee new file mode 100644 index 0000000..1a4979d --- /dev/null +++ b/atom/packages/ex-mode/lib/ex-mode.coffee @@ -0,0 +1,36 @@ +GlobalExState = require './global-ex-state' +ExState = require './ex-state' +Ex = require './ex' +{Disposable, CompositeDisposable} = require 'event-kit' + +module.exports = ExMode = + activate: (state) -> + @globalExState = new GlobalExState + @disposables = new CompositeDisposable + @exStates = new WeakMap + + @disposables.add atom.workspace.observeTextEditors (editor) => + return if editor.mini + + element = atom.views.getView(editor) + + if not @exStates.get(editor) + exState = new ExState( + element, + @globalExState + ) + + @exStates.set(editor, exState) + + @disposables.add new Disposable => + exState.destroy() + + deactivate: -> + @disposables.dispose() + + provideEx: -> + registerCommand: Ex.registerCommand.bind(Ex) + + consumeVim: (vim) -> + @vim = vim + @globalExState.setVim(vim) diff --git a/atom/packages/ex-mode/lib/ex-state.coffee b/atom/packages/ex-mode/lib/ex-state.coffee new file mode 100644 index 0000000..b676535 --- /dev/null +++ b/atom/packages/ex-mode/lib/ex-state.coffee @@ -0,0 +1,59 @@ +{Emitter, Disposable, CompositeDisposable} = require 'event-kit' + +Command = require './command' +CommandError = require './command-error' + +class ExState + constructor: (@editorElement, @globalExState) -> + @emitter = new Emitter + @subscriptions = new CompositeDisposable + @editor = @editorElement.getModel() + @opStack = [] + @history = [] + + @registerOperationCommands + open: (e) => new Command(@editor, @) + + destroy: -> + @subscriptions.dispose() + + getExHistoryItem: (index) -> + @globalExState.commandHistory[index] + + pushExHistory: (command) -> + @globalExState.commandHistory.unshift command + + registerOperationCommands: (commands) -> + for commandName, fn of commands + do (fn) => + pushFn = (e) => @pushOperations(fn(e)) + @subscriptions.add( + atom.commands.add(@editorElement, "ex-mode:#{commandName}", pushFn) + ) + + onDidFailToExecute: (fn) -> + @emitter.on('failed-to-execute', fn) + + pushOperations: (operations) -> + @opStack.push operations + + @processOpStack() if @opStack.length == 2 + + clearOpStack: -> + @opStack = [] + + processOpStack: -> + [command, input] = @opStack + if input.characters.length > 0 + @history.unshift command + try + command.execute(input) + catch e + if (e instanceof CommandError) + atom.notifications.addError("Command error: #{e.message}") + @emitter.emit('failed-to-execute') + else + throw e + @clearOpStack() + +module.exports = ExState diff --git a/atom/packages/ex-mode/lib/ex-view-model.coffee b/atom/packages/ex-mode/lib/ex-view-model.coffee new file mode 100644 index 0000000..0c99be9 --- /dev/null +++ b/atom/packages/ex-mode/lib/ex-view-model.coffee @@ -0,0 +1,35 @@ +{ViewModel, Input} = require './view-model' + +module.exports = +class ExViewModel extends ViewModel + constructor: (@exCommand) -> + super(@exCommand, class: 'command') + @historyIndex = -1 + + atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx) + atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx) + + restoreHistory: (index) -> + @view.editorElement.getModel().setText(@history(index).value) + + history: (index) -> + @exState.getExHistoryItem(index) + + increaseHistoryEx: => + if @history(@historyIndex + 1)? + @historyIndex += 1 + @restoreHistory(@historyIndex) + + decreaseHistoryEx: => + if @historyIndex <= 0 + # get us back to a clean slate + @historyIndex = -1 + @view.editorElement.getModel().setText('') + else + @historyIndex -= 1 + @restoreHistory(@historyIndex) + + confirm: (view) => + @value = @view.value + @exState.pushExHistory(@) + super(view) diff --git a/atom/packages/ex-mode/lib/ex.coffee b/atom/packages/ex-mode/lib/ex.coffee new file mode 100644 index 0000000..c159534 --- /dev/null +++ b/atom/packages/ex-mode/lib/ex.coffee @@ -0,0 +1,231 @@ +path = require 'path' +CommandError = require './command-error' + +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 + +getFullPath = (filePath) -> + return filePath if path.isAbsolute(filePath) + return path.join(atom.project.getPath(), filePath) + +replaceGroups = (groups, replString) -> + arr = replString.split('') + offset = 0 + cdiff = 0 + + while (m = replString.match(/(?:[^\\]|^)\\(\d)/))? + group = groups[m[1]] or '' + i = replString.indexOf(m[0]) + l = m[0].length + replString = replString.slice(i + l) + arr[i + offset...i + offset + l] = (if l is 2 then '' else m[0][0]) + + group + arr = arr.join('').split '' + offset += i + l - group.length + + return arr.join('').replace(/\\\\(\d)/, '\\$1') + +class Ex + @singleton: => + @ex ||= new Ex + + @registerCommand: (name, func) => + @singleton()[name] = func + + quit: -> + atom.workspace.getActivePane().destroyActiveItem() + + q: => @quit() + + tabedit: (range, args) -> + args = args.trim() + filePaths = args.split(' ') + pane = atom.workspace.getActivePane() + if filePaths? and filePaths.length > 0 + for file in filePaths + do -> atom.workspace.openURIInPane file, pane + else + atom.workspace.openURIInPane('', pane) + + tabe: (args...) => @tabedit(args...) + + tabnew: (args...) => @tabedit(args...) + + tabclose: => @quit() + + 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.indexOf(' ') isnt -1 + throw new CommandError('Only one file name allowed') + buffer = atom.workspace.getActiveTextEditor().buffer + filePath = buffer.getPath() if filePath is '' + buffer.setPath(getFullPath(filePath)) + buffer.load() + + e: (args...) => @edit(args...) + + enew: -> + buffer = atom.workspace.getActiveTextEditor().buffer + buffer.setPath(undefined) + buffer.load() + + write: (range, filePath) -> + filePath = filePath.trim() + deferred = Promise.defer() + + pane = atom.workspace.getActivePane() + editor = atom.workspace.getActiveTextEditor() + if atom.workspace.getActiveTextEditor().getPath() isnt undefined + if filePath.length > 0 + editorPath = editor.getPath() + fullPath = getFullPath(filePath) + trySave(-> editor.saveAs(fullPath)) + .then -> + deferred.resolve() + editor.buffer.setPath(editorPath) + else + trySave(-> editor.save()) + .then deferred.resolve + else + if filePath.length > 0 + fullPath = getFullPath(filePath) + trySave(-> editor.saveAs(fullPath)) + .then deferred.resolve + else + fullPath = atom.showSaveDialogSync() + if fullPath? + trySave(-> editor.saveAs(fullPath)) + .then deferred.resolve + + deferred.promise + + w: (args...) => + @write(args...) + + wq: (args...) => + @write(args...).then => @quit() + + x: (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] ?= '' + + 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 + cp = buffer.history.createCheckpoint() + 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])) + ) + buffer.history.groupChangesSinceCheckpoint(cp) + + 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, '') + +module.exports = Ex diff --git a/atom/packages/ex-mode/lib/find.coffee b/atom/packages/ex-mode/lib/find.coffee new file mode 100644 index 0000000..53dffc5 --- /dev/null +++ b/atom/packages/ex-mode/lib/find.coffee @@ -0,0 +1,26 @@ +module.exports = { + findInBuffer : (buffer, pattern) -> + found = [] + buffer.scan(new RegExp(pattern, 'g'), (obj) -> found.push obj.range) + return found + + findNextInBuffer : (buffer, curPos, pattern) -> + found = @findInBuffer(buffer, pattern) + more = (i for i in found when i.compare([curPos, curPos]) is 1) + if more.length > 0 + return more[0].start.row + else if found.length > 0 + return found[0].start.row + else + return null + + findPreviousInBuffer : (buffer, curPos, pattern) -> + found = @findInBuffer(buffer, pattern) + less = (i for i in found when i.compare([curPos, curPos]) is -1) + if less.length > 0 + return less[less.length - 1].start.row + else if found.length > 0 + return found[found.length - 1].start.row + else + return null +} diff --git a/atom/packages/ex-mode/lib/global-ex-state.coffee b/atom/packages/ex-mode/lib/global-ex-state.coffee new file mode 100644 index 0000000..be51b85 --- /dev/null +++ b/atom/packages/ex-mode/lib/global-ex-state.coffee @@ -0,0 +1,5 @@ +class GlobalExState + commandHistory: [] + setVim: (@vim) -> + +module.exports = GlobalExState diff --git a/atom/packages/ex-mode/lib/view-model.coffee b/atom/packages/ex-mode/lib/view-model.coffee new file mode 100644 index 0000000..af96e77 --- /dev/null +++ b/atom/packages/ex-mode/lib/view-model.coffee @@ -0,0 +1,26 @@ +ExCommandModeInputElement = require './ex-command-mode-input-element' + +class ViewModel + constructor: (@command, opts={}) -> + {@editor, @exState} = @command + + @view = new ExCommandModeInputElement().initialize(@, opts) + @editor.commandModeInputView = @view + @exState.onDidFailToExecute => @view.remove() + @done = false + + confirm: (view) -> + @exState.pushOperations(new Input(@view.value)) + @done = true + + cancel: (view) -> + unless @done + @exState.pushOperations(new Input('')) + @done = true + +class Input + constructor: (@characters) -> + +module.exports = { + ViewModel, Input +} |