mirror of
https://github.com/atom/atom.git
synced 2026-02-06 12:44:59 -05:00
Rename App.coffee to Atom.coffee. This also required moving src/atom,spec/atom to src/app,spec/app
This commit is contained in:
21
src/app/ace-outdent-adaptor.coffee
Normal file
21
src/app/ace-outdent-adaptor.coffee
Normal file
@@ -0,0 +1,21 @@
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class AceOutdentAdaptor
|
||||
constructor: (@buffer, @editor) ->
|
||||
|
||||
getLine: (row) ->
|
||||
@buffer.lineForRow(row)
|
||||
|
||||
# We don't care where the bracket is; we always outdent one level
|
||||
findMatchingBracket: ({row, column}) ->
|
||||
{row: 0, column: 0}
|
||||
|
||||
# Does not actually replace text, just line at range.start outdents one level
|
||||
replace: (range, text) ->
|
||||
{row, column} = @editor.getCursorBufferPosition()
|
||||
start = range.start
|
||||
end = {row: range.start.row, column: range.start.column + atom.tabText.length}
|
||||
@buffer.change(new Range(start, end), "")
|
||||
@editor.setCursorBufferPosition({row, column: column - atom.tabText.length})
|
||||
|
||||
47
src/app/anchor.coffee
Normal file
47
src/app/anchor.coffee
Normal file
@@ -0,0 +1,47 @@
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class Anchor
|
||||
editor: null
|
||||
bufferPosition: null
|
||||
screenPosition: null
|
||||
|
||||
constructor: (editor) ->
|
||||
@editor = editor
|
||||
@bufferPosition = new Point(0, 0)
|
||||
@screenPosition = new Point(0, 0)
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
{ oldRange, newRange } = e
|
||||
position = @getBufferPosition()
|
||||
return if position.isLessThan(oldRange.end)
|
||||
|
||||
newRow = newRange.end.row
|
||||
newColumn = newRange.end.column
|
||||
if position.row == oldRange.end.row
|
||||
newColumn += position.column - oldRange.end.column
|
||||
else
|
||||
newColumn = position.column
|
||||
newRow += position.row - oldRange.end.row
|
||||
|
||||
@setBufferPosition [newRow, newColumn]
|
||||
|
||||
getBufferPosition: ->
|
||||
@bufferPosition
|
||||
|
||||
setBufferPosition: (position) ->
|
||||
screenPosition = @editor.screenPositionForBufferPosition(position)
|
||||
@setScreenPosition(screenPosition, clip: false)
|
||||
|
||||
getScreenPosition: ->
|
||||
@screenPosition
|
||||
|
||||
setScreenPosition: (position, options={}) ->
|
||||
position = Point.fromObject(position)
|
||||
clip = options.clip ? true
|
||||
|
||||
@screenPosition = if clip then @editor.clipScreenPosition(position) else position
|
||||
@bufferPosition = @editor.bufferPositionForScreenPosition(position)
|
||||
|
||||
Object.freeze @screenPosition
|
||||
Object.freeze @bufferPosition
|
||||
41
src/app/atom.coffee
Normal file
41
src/app/atom.coffee
Normal file
@@ -0,0 +1,41 @@
|
||||
Keymap = require 'keymap'
|
||||
fs = require 'fs'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
require 'underscore-extensions'
|
||||
|
||||
module.exports =
|
||||
class Atom
|
||||
keymap: null
|
||||
windows: null
|
||||
tabText: null
|
||||
userConfigurationPath: null
|
||||
|
||||
constructor: (@loadPath, nativeMethods)->
|
||||
@windows = []
|
||||
@setUpKeymap()
|
||||
@tabText = " "
|
||||
@userConfigurationPath = fs.absolute "~/.atom/atom.coffee"
|
||||
|
||||
setUpKeymap: ->
|
||||
@keymap = new Keymap()
|
||||
@handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
|
||||
$(document).on 'keydown', @handleKeyEvent
|
||||
@keymap.bindDefaultKeys()
|
||||
|
||||
destroy: ->
|
||||
$(document).off 'keydown', @handleKeyEvent
|
||||
@keymap.unbindDefaultKeys()
|
||||
|
||||
open: (path) ->
|
||||
$native.open path
|
||||
|
||||
quit: ->
|
||||
$native.terminate null
|
||||
|
||||
windowOpened: (window) ->
|
||||
@windows.push(window) unless _.contains(@windows, window)
|
||||
|
||||
windowClosed: (window) ->
|
||||
_.remove(@windows, window)
|
||||
25
src/app/binding-set.coffee
Normal file
25
src/app/binding-set.coffee
Normal file
@@ -0,0 +1,25 @@
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
Specificity = require 'specificity'
|
||||
|
||||
module.exports =
|
||||
class BindingSet
|
||||
selector: null
|
||||
commandForEvent: null
|
||||
|
||||
constructor: (@selector, mapOrFunction) ->
|
||||
@specificity = Specificity(@selector)
|
||||
@commandForEvent = @buildEventHandler(mapOrFunction)
|
||||
|
||||
buildEventHandler: (mapOrFunction) ->
|
||||
if _.isFunction(mapOrFunction)
|
||||
mapOrFunction
|
||||
else
|
||||
(event) =>
|
||||
for pattern, command of mapOrFunction
|
||||
return command if @eventMatchesPattern(event, pattern)
|
||||
null
|
||||
|
||||
eventMatchesPattern: (event, pattern) ->
|
||||
pattern = pattern.replace(/^<|>$/g, '')
|
||||
event.keystroke == pattern
|
||||
225
src/app/buffer.coffee
Normal file
225
src/app/buffer.coffee
Normal file
@@ -0,0 +1,225 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
EventEmitter = require 'event-emitter'
|
||||
UndoManager = require 'undo-manager'
|
||||
|
||||
module.exports =
|
||||
class Buffer
|
||||
@idCounter = 1
|
||||
lines: null
|
||||
path: null
|
||||
|
||||
constructor: (path) ->
|
||||
@id = @constructor.idCounter++
|
||||
@setPath(path)
|
||||
@lines = ['']
|
||||
if @getPath() and fs.exists(@getPath())
|
||||
@setText(fs.read(@getPath()))
|
||||
else
|
||||
@setText('')
|
||||
@undoManager = new UndoManager(this)
|
||||
|
||||
getPath: ->
|
||||
@path
|
||||
|
||||
setPath: (path) ->
|
||||
@path = path
|
||||
@trigger "path-change", this
|
||||
|
||||
getText: ->
|
||||
@lines.join('\n')
|
||||
|
||||
setText: (text) ->
|
||||
@change(@getRange(), text)
|
||||
|
||||
getRange: ->
|
||||
new Range([0, 0], [@getLastRow(), @lastLine().length])
|
||||
|
||||
getTextInRange: (range) ->
|
||||
range = Range.fromObject(range)
|
||||
if range.start.row == range.end.row
|
||||
return @lines[range.start.row][range.start.column...range.end.column]
|
||||
|
||||
multipleLines = []
|
||||
multipleLines.push @lines[range.start.row][range.start.column..] # first line
|
||||
for row in [range.start.row + 1...range.end.row]
|
||||
multipleLines.push @lines[row] # middle lines
|
||||
multipleLines.push @lines[range.end.row][0...range.end.column] # last line
|
||||
|
||||
return multipleLines.join '\n'
|
||||
|
||||
getLines: ->
|
||||
@lines
|
||||
|
||||
lineForRow: (row) ->
|
||||
@lines[row]
|
||||
|
||||
lineLengthForRow: (row) ->
|
||||
@lines[row].length
|
||||
|
||||
rangeForRow: (row) ->
|
||||
new Range([row, 0], [row, @lineLengthForRow(row)])
|
||||
|
||||
numLines: ->
|
||||
@getLines().length
|
||||
|
||||
getLastRow: ->
|
||||
@getLines().length - 1
|
||||
|
||||
lastLine: ->
|
||||
@lineForRow(@getLastRow())
|
||||
|
||||
getEofPosition: ->
|
||||
lastRow = @getLastRow()
|
||||
new Point(lastRow, @lineLengthForRow(lastRow))
|
||||
|
||||
characterIndexForPosition: (position) ->
|
||||
position = Point.fromObject(position)
|
||||
|
||||
index = 0
|
||||
index += @lineLengthForRow(row) + 1 for row in [0...position.row]
|
||||
index + position.column
|
||||
|
||||
positionForCharacterIndex: (index) ->
|
||||
row = 0
|
||||
while index >= (lineLength = @lineLengthForRow(row) + 1)
|
||||
index -= lineLength
|
||||
row++
|
||||
|
||||
new Point(row, index)
|
||||
|
||||
deleteRow: (row) ->
|
||||
range = null
|
||||
if row == @getLastRow()
|
||||
range = new Range([row - 1, @lineLengthForRow(row - 1)], [row, @lineLengthForRow(row)])
|
||||
else
|
||||
range = new Range([row, 0], [row + 1, 0])
|
||||
|
||||
@change(range, '')
|
||||
|
||||
insert: (point, text) ->
|
||||
@change(new Range(point, point), text)
|
||||
|
||||
delete: (range) ->
|
||||
@change(range, '')
|
||||
|
||||
change: (oldRange, newText) ->
|
||||
oldRange = Range.fromObject(oldRange)
|
||||
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
|
||||
prefix = @lines[oldRange.start.row][0...oldRange.start.column]
|
||||
suffix = @lines[oldRange.end.row][oldRange.end.column..]
|
||||
oldText = @getTextInRange(oldRange)
|
||||
|
||||
newTextLines = newText.split('\n')
|
||||
|
||||
if newTextLines.length == 1
|
||||
newRange.end.column += newText.length
|
||||
newTextLines = [prefix + newText + suffix]
|
||||
else
|
||||
lastLineIndex = newTextLines.length - 1
|
||||
newTextLines[0] = prefix + newTextLines[0]
|
||||
newRange.end.row += lastLineIndex
|
||||
newRange.end.column = newTextLines[lastLineIndex].length
|
||||
newTextLines[lastLineIndex] += suffix
|
||||
|
||||
@lines[oldRange.start.row..oldRange.end.row] = newTextLines
|
||||
@trigger 'change', { oldRange, newRange, oldText, newText }
|
||||
|
||||
undo: ->
|
||||
@undoManager.undo()
|
||||
|
||||
redo: ->
|
||||
@undoManager.redo()
|
||||
|
||||
save: ->
|
||||
if not @getPath() then throw new Error("Tried to save buffer with no file path")
|
||||
fs.write @getPath(), @getText()
|
||||
|
||||
saveAs: (path) ->
|
||||
@setPath(path)
|
||||
@save()
|
||||
|
||||
getMode: ->
|
||||
return @mode if @mode
|
||||
extension = if @getPath() then @getPath().split('/').pop().split('.').pop() else null
|
||||
modeName = switch extension
|
||||
when 'js' then 'javascript'
|
||||
when 'coffee' then 'coffee'
|
||||
when 'rb', 'ru' then 'ruby'
|
||||
when 'c', 'h', 'cpp' then 'c_cpp'
|
||||
when 'html', 'htm' then 'html'
|
||||
when 'css' then 'css'
|
||||
else 'text'
|
||||
|
||||
@mode = new (require("ace/mode/#{modeName}").Mode)
|
||||
|
||||
traverseRegexMatchesInRange: (regex, range, iterator) ->
|
||||
range = Range.fromObject(range)
|
||||
global = regex.global
|
||||
regex = new RegExp(regex.source, 'gm')
|
||||
|
||||
traverseRecursively = (text, startIndex, endIndex, lengthDelta) =>
|
||||
regex.lastIndex = startIndex
|
||||
return unless match = regex.exec(text)
|
||||
|
||||
matchLength = match[0].length
|
||||
matchStartIndex = match.index
|
||||
matchEndIndex = matchStartIndex + matchLength
|
||||
|
||||
if matchEndIndex > endIndex
|
||||
regex.lastIndex = 0
|
||||
if matchStartIndex < endIndex and match = regex.exec(text[matchStartIndex...endIndex])
|
||||
matchLength = match[0].length
|
||||
matchEndIndex = matchStartIndex + matchLength
|
||||
else
|
||||
return
|
||||
|
||||
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
|
||||
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
|
||||
range = new Range(startPosition, endPosition)
|
||||
recurse = true
|
||||
replacementText = null
|
||||
stop = -> recurse = false
|
||||
replace = (text) -> replacementText = text
|
||||
iterator(match, range, { stop, replace })
|
||||
|
||||
if replacementText
|
||||
@change(range, replacementText)
|
||||
lengthDelta += replacementText.length - matchLength
|
||||
|
||||
if matchLength is 0
|
||||
matchStartIndex++
|
||||
matchEndIndex++
|
||||
|
||||
if global and recurse
|
||||
traverseRecursively(text, matchEndIndex, endIndex, lengthDelta)
|
||||
|
||||
startIndex = @characterIndexForPosition(range.start)
|
||||
endIndex = @characterIndexForPosition(range.end)
|
||||
traverseRecursively(@getText(), startIndex, endIndex, 0)
|
||||
|
||||
backwardsTraverseRegexMatchesInRange: (regex, range, iterator) ->
|
||||
global = regex.global
|
||||
regex = new RegExp(regex.source, 'gm')
|
||||
|
||||
matches = []
|
||||
@traverseRegexMatchesInRange regex, range, (match, matchRange) ->
|
||||
matches.push([match, matchRange])
|
||||
|
||||
matches.reverse()
|
||||
|
||||
recurse = true
|
||||
stop = -> recurse = false
|
||||
replacementText = null
|
||||
replace = (text) -> replacementText = text
|
||||
|
||||
for [match, matchRange] in matches
|
||||
replacementText = null
|
||||
iterator(match, matchRange, { stop, replace })
|
||||
@change(matchRange, replacementText) if replacementText
|
||||
return unless global and recurse
|
||||
|
||||
|
||||
_.extend(Buffer.prototype, EventEmitter)
|
||||
16
src/app/command-interpreter.coffee
Normal file
16
src/app/command-interpreter.coffee
Normal file
@@ -0,0 +1,16 @@
|
||||
fs = require 'fs'
|
||||
PEG = require 'pegjs'
|
||||
|
||||
module.exports =
|
||||
class CommandInterpreter
|
||||
constructor: ->
|
||||
@parser = PEG.buildParser(fs.read(require.resolve 'commands.pegjs'))
|
||||
|
||||
eval: (editor, string) ->
|
||||
command = @parser.parse(string)
|
||||
@lastRelativeAddress = command if command.isRelativeAddress()
|
||||
command.execute(editor)
|
||||
|
||||
repeatRelativeAddress: (editor) ->
|
||||
@lastRelativeAddress?.execute(editor)
|
||||
|
||||
12
src/app/command-interpreter/address-range.coffee
Normal file
12
src/app/command-interpreter/address-range.coffee
Normal file
@@ -0,0 +1,12 @@
|
||||
Address = require 'command-interpreter/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class AddressRange extends Address
|
||||
constructor: (@startAddress, @endAddress) ->
|
||||
|
||||
getRange: (editor) ->
|
||||
new Range(@startAddress.getRange(editor).start, @endAddress.getRange(editor).end)
|
||||
|
||||
isRelative: ->
|
||||
@startAddress.isRelative() or @endAddress.isRelative()
|
||||
10
src/app/command-interpreter/address.coffee
Normal file
10
src/app/command-interpreter/address.coffee
Normal file
@@ -0,0 +1,10 @@
|
||||
Command = require 'command-interpreter/command'
|
||||
|
||||
module.exports =
|
||||
class Address extends Command
|
||||
execute: (editor) ->
|
||||
range = @getRange(editor)
|
||||
editor.clearSelections()
|
||||
editor.setSelectionBufferRange(range)
|
||||
|
||||
isAddress: -> true
|
||||
5
src/app/command-interpreter/command.coffee
Normal file
5
src/app/command-interpreter/command.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Command
|
||||
isAddress: -> false
|
||||
12
src/app/command-interpreter/composite-command.coffee
Normal file
12
src/app/command-interpreter/composite-command.coffee
Normal file
@@ -0,0 +1,12 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class CompositeCommand
|
||||
constructor: (@subcommands) ->
|
||||
|
||||
execute: (editor) ->
|
||||
command.execute(editor) for command in @subcommands
|
||||
|
||||
isRelativeAddress: ->
|
||||
_.all(@subcommands, (command) -> command.isAddress() and command.isRelative())
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Address = require 'command-interpreter/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class CurrentSelectionAddress extends Address
|
||||
getRange: (editor) ->
|
||||
editor.getSelection().getBufferRange()
|
||||
|
||||
isRelative: -> true
|
||||
11
src/app/command-interpreter/eof-address.coffee
Normal file
11
src/app/command-interpreter/eof-address.coffee
Normal file
@@ -0,0 +1,11 @@
|
||||
Address = require 'command-interpreter/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class EofAddress extends Address
|
||||
getRange: (editor) ->
|
||||
lastRow = editor.getLastBufferRow()
|
||||
column = editor.lineLengthForBufferRow(lastRow)
|
||||
new Range([lastRow, column], [lastRow, column])
|
||||
|
||||
isRelative: -> false
|
||||
12
src/app/command-interpreter/line-address.coffee
Normal file
12
src/app/command-interpreter/line-address.coffee
Normal file
@@ -0,0 +1,12 @@
|
||||
Address = require 'command-interpreter/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class LineAddress extends Address
|
||||
constructor: (lineNumber) ->
|
||||
@row = lineNumber - 1
|
||||
|
||||
getRange: ->
|
||||
new Range([@row, 0], [@row + 1, 0])
|
||||
|
||||
isRelative: -> false
|
||||
28
src/app/command-interpreter/regex-address.coffee
Normal file
28
src/app/command-interpreter/regex-address.coffee
Normal file
@@ -0,0 +1,28 @@
|
||||
Address = require 'command-interpreter/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class RegexAddress extends Address
|
||||
regex: null
|
||||
|
||||
constructor: (pattern) ->
|
||||
@regex = new RegExp(pattern)
|
||||
|
||||
getRange: (editor) ->
|
||||
selectedRange = editor.getLastSelectionInBuffer().getBufferRange()
|
||||
rangeToSearch = new Range(selectedRange.end, editor.getEofPosition())
|
||||
|
||||
rangeToReturn = null
|
||||
editor.buffer.traverseRegexMatchesInRange @regex, rangeToSearch, (match, range) ->
|
||||
rangeToReturn = range
|
||||
|
||||
if rangeToReturn
|
||||
rangeToReturn
|
||||
else
|
||||
rangeToSearch = new Range([0, 0], rangeToSearch.start)
|
||||
editor.buffer.traverseRegexMatchesInRange @regex, rangeToSearch, (match, range) ->
|
||||
rangeToReturn = range
|
||||
|
||||
rangeToReturn or selectedRange
|
||||
|
||||
isRelative: -> true
|
||||
19
src/app/command-interpreter/select-all-matches.coffee
Normal file
19
src/app/command-interpreter/select-all-matches.coffee
Normal file
@@ -0,0 +1,19 @@
|
||||
Command = require 'command-interpreter/command'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class SelectAllMatches extends Command
|
||||
regex: null
|
||||
|
||||
constructor: (pattern) ->
|
||||
@regex = new RegExp(pattern, 'g')
|
||||
|
||||
execute: (editor) ->
|
||||
rangesToSelect = []
|
||||
for selection in editor.getSelections()
|
||||
editor.buffer.traverseRegexMatchesInRange @regex, selection.getBufferRange(), (match, range) ->
|
||||
rangesToSelect.push(range)
|
||||
|
||||
editor.clearSelections()
|
||||
editor.setSelectionBufferRange(rangesToSelect[0])
|
||||
editor.addSelectionForBufferRange(range) for range in rangesToSelect[1..]
|
||||
16
src/app/command-interpreter/substitution.coffee
Normal file
16
src/app/command-interpreter/substitution.coffee
Normal file
@@ -0,0 +1,16 @@
|
||||
Command = require 'command-interpreter/command'
|
||||
|
||||
module.exports =
|
||||
class Substitution extends Command
|
||||
regex: null
|
||||
replacementText: null
|
||||
|
||||
constructor: (pattern, replacementText, options) ->
|
||||
@replacementText = replacementText
|
||||
@regex = new RegExp(pattern, options.join(''))
|
||||
|
||||
execute: (editor) ->
|
||||
range = editor.getSelection().getBufferRange()
|
||||
editor.buffer.traverseRegexMatchesInRange @regex, range, (match, matchRange, { replace }) =>
|
||||
replace(@replacementText)
|
||||
|
||||
72
src/app/command-panel.coffee
Normal file
72
src/app/command-panel.coffee
Normal file
@@ -0,0 +1,72 @@
|
||||
{View} = require 'space-pen'
|
||||
CommandInterpreter = require 'command-interpreter'
|
||||
Editor = require 'editor'
|
||||
{SyntaxError} = require('pegjs').parser
|
||||
|
||||
module.exports =
|
||||
class CommandPanel extends View
|
||||
@content: ->
|
||||
@div class: 'command-panel', =>
|
||||
@div ':', class: 'prompt', outlet: 'prompt'
|
||||
@subview 'editor', new Editor
|
||||
|
||||
commandInterpreter: null
|
||||
history: null
|
||||
historyIndex: 0
|
||||
|
||||
initialize: ({@rootView})->
|
||||
requireStylesheet 'command-panel.css'
|
||||
|
||||
@commandInterpreter = new CommandInterpreter()
|
||||
@history = []
|
||||
|
||||
@rootView.on 'command-panel:toggle', => @toggle()
|
||||
@rootView.on 'command-panel:execute', => @execute()
|
||||
@rootView.on 'command-panel:repeat-relative-address', => @repeatRelativeAddress()
|
||||
@editor.addClass 'single-line'
|
||||
|
||||
@editor.off 'move-up move-down'
|
||||
@editor.on 'move-up', => @navigateBackwardInHistory()
|
||||
@editor.on 'move-down', => @navigateForwardInHistory()
|
||||
|
||||
toggle: ->
|
||||
if @parent().length then @hide() else @show()
|
||||
|
||||
show: ->
|
||||
@rootView.append(this)
|
||||
@prompt.css 'font', @editor.css('font')
|
||||
@editor.focus()
|
||||
@editor.buffer.setText('')
|
||||
|
||||
hide: ->
|
||||
@detach()
|
||||
@rootView.activeEditor().focus()
|
||||
|
||||
execute: (command = @editor.getText()) ->
|
||||
try
|
||||
@commandInterpreter.eval(@rootView.activeEditor(), command)
|
||||
catch error
|
||||
if error instanceof SyntaxError
|
||||
@addClass 'error'
|
||||
removeErrorClass = => @removeClass 'error'
|
||||
window.setTimeout(removeErrorClass, 200)
|
||||
return
|
||||
else
|
||||
throw error
|
||||
|
||||
@history.push(command)
|
||||
@historyIndex = @history.length
|
||||
@hide()
|
||||
|
||||
navigateBackwardInHistory: ->
|
||||
return if @historyIndex == 0
|
||||
@historyIndex--
|
||||
@editor.setText(@history[@historyIndex])
|
||||
|
||||
navigateForwardInHistory: ->
|
||||
return if @historyIndex == @history.length
|
||||
@historyIndex++
|
||||
@editor.setText(@history[@historyIndex] or '')
|
||||
|
||||
repeatRelativeAddress: ->
|
||||
@commandInterpreter.repeatRelativeAddress(@rootView.activeEditor())
|
||||
49
src/app/commands.pegjs
Normal file
49
src/app/commands.pegjs
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
var CompositeCommand = require('command-interpreter/composite-command')
|
||||
var Substitution = require('command-interpreter/substitution');
|
||||
var LineAddress = require('command-interpreter/line-address');
|
||||
var AddressRange = require('command-interpreter/address-range');
|
||||
var EofAddress = require('command-interpreter/eof-address');
|
||||
var CurrentSelectionAddress = require('command-interpreter/current-selection-address')
|
||||
var RegexAddress = require('command-interpreter/regex-address')
|
||||
var SelectAllMatches = require('command-interpreter/select-all-matches')
|
||||
}
|
||||
|
||||
start = expressions:(expression+) {
|
||||
return new CompositeCommand(expressions)
|
||||
}
|
||||
|
||||
expression = _ expression:(address / command) _ { return expression; }
|
||||
|
||||
address = addressRange / primitiveAddress
|
||||
|
||||
addressRange
|
||||
= start:primitiveAddress? _ ',' _ end:address? {
|
||||
if (!start) start = new LineAddress(0)
|
||||
if (!end) end = new EofAddress()
|
||||
return new AddressRange(start, end)
|
||||
}
|
||||
|
||||
primitiveAddress
|
||||
= lineNumber:integer { return new LineAddress(lineNumber) }
|
||||
/ '$' { return new EofAddress() }
|
||||
/ '.' { return new CurrentSelectionAddress() }
|
||||
/ '/' pattern:pattern '/'? { return new RegexAddress(pattern)}
|
||||
|
||||
command = substitution / selectAllMatches
|
||||
|
||||
substitution
|
||||
= "s" _ "/" find:pattern "/" replace:pattern "/" _ options:[g]* {
|
||||
return new Substitution(find, replace, options);
|
||||
}
|
||||
|
||||
selectAllMatches
|
||||
= 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatches(pattern) }
|
||||
|
||||
pattern
|
||||
= pattern:[^/]* { return pattern.join('') }
|
||||
|
||||
integer
|
||||
= digits:[0-9]+ { return parseInt(digits.join('')); }
|
||||
|
||||
_ = " "*
|
||||
93
src/app/composite-cursor.coffee
Normal file
93
src/app/composite-cursor.coffee
Normal file
@@ -0,0 +1,93 @@
|
||||
Cursor = require 'cursor'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class CompositeCursor
|
||||
constructor: (@editor) ->
|
||||
@cursors = []
|
||||
@addCursor()
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
@moveCursors (cursor) -> cursor.handleBufferChange(e)
|
||||
|
||||
getCursor: (index) ->
|
||||
index ?= @cursors.length - 1
|
||||
@cursors[index]
|
||||
|
||||
getCursors: ->
|
||||
@cursors
|
||||
|
||||
addCursor: ->
|
||||
cursor = new Cursor(@editor)
|
||||
@cursors.push(cursor)
|
||||
@editor.lines.append(cursor)
|
||||
cursor
|
||||
|
||||
addCursorAtScreenPosition: (screenPosition) ->
|
||||
cursor = @addCursor()
|
||||
cursor.setScreenPosition(screenPosition)
|
||||
|
||||
addCursorAtBufferPosition: (bufferPosition) ->
|
||||
cursor = @addCursor()
|
||||
cursor.setBufferPosition(bufferPosition)
|
||||
|
||||
removeCursor: (cursor) ->
|
||||
_.remove(@cursors, cursor)
|
||||
|
||||
moveCursors: (fn) ->
|
||||
fn(cursor) for cursor in @cursors
|
||||
@mergeCursors()
|
||||
|
||||
setScreenPosition: (screenPosition) ->
|
||||
@moveCursors (cursor) -> cursor.setScreenPosition(screenPosition)
|
||||
|
||||
setBufferPosition: (bufferPosition) ->
|
||||
@moveCursors (cursor) -> cursor.setBufferPosition(bufferPosition)
|
||||
|
||||
updateBufferPosition: ->
|
||||
@moveCursors (cursor) -> cursor.setBufferPosition(cursor.getBufferPosition())
|
||||
|
||||
moveLeft: ->
|
||||
@moveCursors (cursor) -> cursor.moveLeft()
|
||||
|
||||
moveRight: ->
|
||||
@moveCursors (cursor) -> cursor.moveRight()
|
||||
|
||||
moveUp: ->
|
||||
@moveCursors (cursor) -> cursor.moveUp()
|
||||
|
||||
moveDown: ->
|
||||
@moveCursors (cursor) -> cursor.moveDown()
|
||||
|
||||
moveToNextWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToNextWord()
|
||||
|
||||
moveToBeginningOfWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBeginningOfWord()
|
||||
|
||||
moveToEndOfWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToEndOfWord()
|
||||
|
||||
moveToTop: ->
|
||||
@moveCursors (cursor) -> cursor.moveToTop()
|
||||
|
||||
moveToBottom: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBottom()
|
||||
|
||||
moveToBeginningOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBeginningOfLine()
|
||||
|
||||
moveToEndOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToEndOfLine()
|
||||
|
||||
moveToFirstCharacterOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()
|
||||
|
||||
mergeCursors: ->
|
||||
positions = []
|
||||
for cursor in new Array(@cursors...)
|
||||
position = cursor.getBufferPosition().toString()
|
||||
if position in positions
|
||||
cursor.remove()
|
||||
else
|
||||
positions.push(position)
|
||||
138
src/app/composite-selection.coffee
Normal file
138
src/app/composite-selection.coffee
Normal file
@@ -0,0 +1,138 @@
|
||||
Selection = require 'selection'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class CompositeSeleciton
|
||||
constructor: (@editor) ->
|
||||
@selections = []
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
selection.handleBufferChange(e) for selection in @getSelections()
|
||||
|
||||
getSelection: (index) ->
|
||||
index ?= @selections.length - 1
|
||||
@selections[index]
|
||||
|
||||
getSelections: ->
|
||||
new Array(@selections...)
|
||||
|
||||
getLastSelection: ->
|
||||
_.last(@selections)
|
||||
|
||||
getLastSelectionInBuffer: ->
|
||||
_.last(@getSelections().sort (a, b) ->
|
||||
aRange = a.getBufferRange()
|
||||
bRange = b.getBufferRange()
|
||||
aRange.end.compare(bRange.end))
|
||||
|
||||
clearSelections: ->
|
||||
for selection in @getSelections()[1..]
|
||||
selection.cursor.remove()
|
||||
|
||||
@getLastSelection().clearSelection()
|
||||
|
||||
addSelectionForCursor: (cursor) ->
|
||||
selection = new Selection({@editor, cursor})
|
||||
@selections.push(selection)
|
||||
@editor.lines.append(selection)
|
||||
selection
|
||||
|
||||
addSelectionForBufferRange: (bufferRange, options) ->
|
||||
cursor = @editor.compositeCursor.addCursor()
|
||||
@selectionForCursor(cursor).setBufferRange(bufferRange, options)
|
||||
|
||||
removeSelectionForCursor: (cursor) ->
|
||||
selection = @selectionForCursor(cursor)
|
||||
selection.cursor = null
|
||||
selection.remove()
|
||||
_.remove(@selections, selection)
|
||||
|
||||
selectionForCursor: (cursor) ->
|
||||
_.find @selections, (selection) -> selection.cursor == cursor
|
||||
|
||||
setBufferRange: (bufferRange, options) ->
|
||||
@getLastSelection().setBufferRange(bufferRange, options)
|
||||
|
||||
getBufferRange: (bufferRange) ->
|
||||
@getLastSelection().getBufferRange()
|
||||
|
||||
getText: ->
|
||||
@getLastSelection().getText()
|
||||
|
||||
expandSelectionsForward: (fn) ->
|
||||
fn(selection) for selection in @getSelections()
|
||||
@mergeIntersectingSelections()
|
||||
|
||||
expandSelectionsBackward: (fn) ->
|
||||
fn(selection) for selection in @getSelections()
|
||||
@mergeIntersectingSelections(reverse: true)
|
||||
|
||||
insertText: (text) ->
|
||||
selection.insertText(text) for selection in @getSelections()
|
||||
|
||||
backspace: ->
|
||||
selection.backspace() for selection in @getSelections()
|
||||
|
||||
backspaceToBeginningOfWord: ->
|
||||
selection.backspaceToBeginningOfWord() for selection in @getSelections()
|
||||
|
||||
delete: ->
|
||||
selection.delete() for selection in @getSelections()
|
||||
|
||||
deleteToEndOfWord: ->
|
||||
selection.deleteToEndOfWord() for selection in @getSelections()
|
||||
|
||||
selectToScreenPosition: (position) ->
|
||||
@getLastSelection().selectToScreenPosition(position)
|
||||
|
||||
selectRight: ->
|
||||
@expandSelectionsForward (selection) => selection.selectRight()
|
||||
|
||||
selectLeft: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectLeft()
|
||||
|
||||
selectUp: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectUp()
|
||||
|
||||
selectDown: ->
|
||||
@expandSelectionsForward (selection) => selection.selectDown()
|
||||
|
||||
selectToTop: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectToTop()
|
||||
|
||||
selectToBottom: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToBottom()
|
||||
|
||||
selectToBeginningOfLine: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectToBeginningOfLine()
|
||||
|
||||
selectToEndOfLine: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToEndOfLine()
|
||||
|
||||
selectToBeginningOfWord: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectToBeginningOfWord()
|
||||
|
||||
selectToEndOfWord: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToEndOfWord()
|
||||
|
||||
cut: ->
|
||||
maintainPasteboard = false
|
||||
for selection in @getSelections()
|
||||
selection.cut(maintainPasteboard)
|
||||
maintainPasteboard = true
|
||||
|
||||
copy: ->
|
||||
maintainPasteboard = false
|
||||
for selection in @getSelections()
|
||||
selection.copy(maintainPasteboard)
|
||||
maintainPasteboard = true
|
||||
|
||||
mergeIntersectingSelections: (options) ->
|
||||
for selection in @getSelections()
|
||||
otherSelections = @getSelections()
|
||||
_.remove(otherSelections, selection)
|
||||
for otherSelection in otherSelections
|
||||
if selection.intersectsWith(otherSelection)
|
||||
selection.merge(otherSelection, options)
|
||||
@mergeIntersectingSelections(options)
|
||||
return
|
||||
168
src/app/cursor.coffee
Normal file
168
src/app/cursor.coffee
Normal file
@@ -0,0 +1,168 @@
|
||||
{View} = require 'space-pen'
|
||||
Anchor = require 'anchor'
|
||||
Point = require 'point'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Cursor extends View
|
||||
@content: ->
|
||||
@pre class: 'cursor idle', => @raw ' '
|
||||
|
||||
anchor: null
|
||||
editor: null
|
||||
wordRegex: /(\w+)|([^\w\s]+)/g
|
||||
|
||||
initialize: (@editor) ->
|
||||
@anchor = new Anchor(@editor)
|
||||
@selection = @editor.compositeSelection.addSelectionForCursor(this)
|
||||
@one 'attach', => @updateAppearance()
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
@anchor.handleBufferChange(e)
|
||||
@refreshScreenPosition()
|
||||
|
||||
remove: ->
|
||||
@editor.compositeCursor.removeCursor(this)
|
||||
@editor.compositeSelection.removeSelectionForCursor(this)
|
||||
super
|
||||
|
||||
getBufferPosition: ->
|
||||
@anchor.getBufferPosition()
|
||||
|
||||
setBufferPosition: (bufferPosition) ->
|
||||
@anchor.setBufferPosition(bufferPosition)
|
||||
@refreshScreenPosition()
|
||||
@clearSelection()
|
||||
|
||||
getScreenPosition: ->
|
||||
@anchor.getScreenPosition()
|
||||
|
||||
setScreenPosition: (position, options={}) ->
|
||||
@anchor.setScreenPosition(position, options)
|
||||
@refreshScreenPosition(position, options)
|
||||
@clearSelection()
|
||||
|
||||
refreshScreenPosition: ->
|
||||
@goalColumn = null
|
||||
@updateAppearance()
|
||||
@trigger 'cursor:position-changed'
|
||||
|
||||
@removeClass 'idle'
|
||||
window.clearTimeout(@idleTimeout) if @idleTimeout
|
||||
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
|
||||
|
||||
clearSelection: ->
|
||||
@selection.clearSelection() unless @selection.retainSelection
|
||||
|
||||
getCurrentBufferLine: ->
|
||||
@editor.lineForBufferRow(@getBufferPosition().row)
|
||||
|
||||
isOnEOL: ->
|
||||
@getScreenPosition().column == @getCurrentBufferLine().length
|
||||
|
||||
moveUp: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row - 1, column: column})
|
||||
@goalColumn = column
|
||||
|
||||
moveDown: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row + 1, column: column})
|
||||
@goalColumn = column
|
||||
|
||||
moveToNextWord: ->
|
||||
bufferPosition = @getBufferPosition()
|
||||
range = [bufferPosition, @editor.getEofPosition()]
|
||||
|
||||
nextPosition = null
|
||||
@editor.traverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
|
||||
if matchRange.start.isGreaterThan(bufferPosition)
|
||||
nextPosition = matchRange.start
|
||||
stop()
|
||||
|
||||
@setBufferPosition(nextPosition or @editor.getEofPosition())
|
||||
|
||||
moveToBeginningOfWord: ->
|
||||
bufferPosition = @getBufferPosition()
|
||||
range = [[0,0], bufferPosition]
|
||||
@editor.backwardsTraverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
|
||||
@setBufferPosition matchRange.start
|
||||
stop()
|
||||
|
||||
moveToEndOfWord: ->
|
||||
bufferPosition = @getBufferPosition()
|
||||
range = [bufferPosition, @editor.getEofPosition()]
|
||||
|
||||
@editor.traverseRegexMatchesInRange @wordRegex, range, (match, matchRange, { stop }) =>
|
||||
@setBufferPosition matchRange.end
|
||||
stop()
|
||||
|
||||
moveToEndOfLine: ->
|
||||
{ row } = @getBufferPosition()
|
||||
@setBufferPosition({ row, column: @editor.buffer.lineForRow(row).length })
|
||||
|
||||
moveToBeginningOfLine: ->
|
||||
{ row } = @getScreenPosition()
|
||||
@setScreenPosition({ row, column: 0 })
|
||||
|
||||
moveToFirstCharacterOfLine: ->
|
||||
position = @getBufferPosition()
|
||||
range = @editor.rangeForBufferRow(position.row)
|
||||
newPosition = null
|
||||
@editor.traverseRegexMatchesInRange /^\s*/, range, (match, matchRange) =>
|
||||
newPosition = matchRange.end
|
||||
newPosition = [position.row, 0] if newPosition.isEqual(position)
|
||||
@setBufferPosition(newPosition)
|
||||
|
||||
moveRight: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
@setScreenPosition(@editor.clipScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true))
|
||||
|
||||
moveLeft: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
[row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
|
||||
@setScreenPosition({row, column})
|
||||
|
||||
moveToTop: ->
|
||||
@setBufferPosition [0,0]
|
||||
|
||||
moveToBottom: ->
|
||||
@setBufferPosition @editor.getEofPosition()
|
||||
|
||||
updateAppearance: ->
|
||||
position = @editor.pixelPositionForScreenPosition(@getScreenPosition())
|
||||
@css(position)
|
||||
_.defer =>
|
||||
@autoScrollVertically(position)
|
||||
@autoScrollHorizontally(position)
|
||||
|
||||
autoScrollVertically: (position) ->
|
||||
linesInView = @editor.height() / @height()
|
||||
maxScrollMargin = Math.floor((linesInView - 1) / 2)
|
||||
scrollMargin = Math.min(@editor.vScrollMargin, maxScrollMargin)
|
||||
margin = scrollMargin * @height()
|
||||
desiredTop = position.top - margin
|
||||
desiredBottom = position.top + @height() + margin
|
||||
|
||||
if desiredBottom > @editor.scrollBottom()
|
||||
@editor.scrollBottom(desiredBottom)
|
||||
else if desiredTop < @editor.scrollTop()
|
||||
@editor.scrollTop(desiredTop)
|
||||
|
||||
autoScrollHorizontally: (position) ->
|
||||
return if @editor.softWrap
|
||||
|
||||
charsInView = @editor.horizontalScroller.width() / @width()
|
||||
maxScrollMargin = Math.floor((charsInView - 1) / 2)
|
||||
scrollMargin = Math.min(@editor.hScrollMargin, maxScrollMargin)
|
||||
margin = scrollMargin * @width()
|
||||
desiredRight = position.left + @width() + margin
|
||||
desiredLeft = position.left - margin
|
||||
|
||||
if desiredRight > @editor.horizontalScroller.scrollRight()
|
||||
@editor.horizontalScroller.scrollRight(desiredRight)
|
||||
else if desiredLeft < @editor.horizontalScroller.scrollLeft()
|
||||
@editor.horizontalScroller.scrollLeft(desiredLeft)
|
||||
|
||||
8
src/app/edit-session.coffee
Normal file
8
src/app/edit-session.coffee
Normal file
@@ -0,0 +1,8 @@
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class EditSession
|
||||
cursorScreenPosition: new Point(0, 0)
|
||||
scrollTop: 0
|
||||
scrollLeft: 0
|
||||
|
||||
462
src/app/editor.coffee
Normal file
462
src/app/editor.coffee
Normal file
@@ -0,0 +1,462 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
Buffer = require 'buffer'
|
||||
CompositeCursor = require 'composite-cursor'
|
||||
CompositeSelection = require 'composite-selection'
|
||||
Gutter = require 'gutter'
|
||||
Renderer = require 'renderer'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
EditSession = require 'edit-session'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Editor extends View
|
||||
@idCounter: 1
|
||||
|
||||
@content: ->
|
||||
@div class: 'editor', tabindex: -1, =>
|
||||
@div class: 'scrollable-content', =>
|
||||
@subview 'gutter', new Gutter
|
||||
@div class: 'horizontal-scroller', outlet: 'horizontalScroller', =>
|
||||
@div class: 'lines', outlet: 'lines', =>
|
||||
@input class: 'hidden-input', outlet: 'hiddenInput'
|
||||
|
||||
vScrollMargin: 2
|
||||
hScrollMargin: 10
|
||||
softWrap: false
|
||||
lineHeight: null
|
||||
charWidth: null
|
||||
charHeight: null
|
||||
cursor: null
|
||||
selection: null
|
||||
buffer: null
|
||||
highlighter: null
|
||||
renderer: null
|
||||
autoIndent: null
|
||||
lineCache: null
|
||||
isFocused: false
|
||||
|
||||
initialize: ({buffer}) ->
|
||||
requireStylesheet 'editor.css'
|
||||
requireStylesheet 'theme/twilight.css'
|
||||
|
||||
@id = Editor.idCounter++
|
||||
@editSessionsByBufferId = {}
|
||||
@bindKeys()
|
||||
@buildCursorAndSelection()
|
||||
@handleEvents()
|
||||
@setBuffer(buffer ? new Buffer)
|
||||
@autoIndent = true
|
||||
|
||||
bindKeys: ->
|
||||
@on 'save', => @save()
|
||||
@on 'move-right', => @moveCursorRight()
|
||||
@on 'move-left', => @moveCursorLeft()
|
||||
@on 'move-down', => @moveCursorDown()
|
||||
@on 'move-up', => @moveCursorUp()
|
||||
@on 'move-to-next-word', => @moveCursorToNextWord()
|
||||
@on 'move-to-previous-word', => @moveCursorToPreviousWord()
|
||||
@on 'select-right', => @selectRight()
|
||||
@on 'select-left', => @selectLeft()
|
||||
@on 'select-up', => @selectUp()
|
||||
@on 'select-down', => @selectDown()
|
||||
@on 'newline', => @insertText("\n")
|
||||
@on 'backspace', => @backspace()
|
||||
@on 'backspace-to-beginning-of-word', => @backspaceToBeginningOfWord()
|
||||
@on 'delete', => @delete()
|
||||
@on 'delete-to-end-of-word', => @deleteToEndOfWord()
|
||||
@on 'cut', => @cutSelection()
|
||||
@on 'copy', => @copySelection()
|
||||
@on 'paste', => @paste()
|
||||
@on 'undo', => @undo()
|
||||
@on 'redo', => @redo()
|
||||
@on 'toggle-soft-wrap', => @toggleSoftWrap()
|
||||
@on 'fold-selection', => @foldSelection()
|
||||
@on 'split-left', => @splitLeft()
|
||||
@on 'split-right', => @splitRight()
|
||||
@on 'split-up', => @splitUp()
|
||||
@on 'split-down', => @splitDown()
|
||||
@on 'close', => @remove(); false
|
||||
|
||||
@on 'move-to-top', => @moveCursorToTop()
|
||||
@on 'move-to-bottom', => @moveCursorToBottom()
|
||||
@on 'move-to-beginning-of-line', => @moveCursorToBeginningOfLine()
|
||||
@on 'move-to-end-of-line', => @moveCursorToEndOfLine()
|
||||
@on 'move-to-first-character-of-line', => @moveCursorToFirstCharacterOfLine()
|
||||
@on 'move-to-beginning-of-word', => @moveCursorToBeginningOfWord()
|
||||
@on 'move-to-end-of-word', => @moveCursorToEndOfWord()
|
||||
@on 'select-to-top', => @selectToTop()
|
||||
@on 'select-to-bottom', => @selectToBottom()
|
||||
@on 'select-to-end-of-line', => @selectToEndOfLine()
|
||||
@on 'select-to-beginning-of-line', => @selectToBeginningOfLine()
|
||||
@on 'select-to-end-of-word', => @selectToEndOfWord()
|
||||
@on 'select-to-beginning-of-word', => @selectToBeginningOfWord()
|
||||
|
||||
buildCursorAndSelection: ->
|
||||
@compositeSelection = new CompositeSelection(this)
|
||||
@compositeCursor = new CompositeCursor(this)
|
||||
|
||||
addCursorAtScreenPosition: (screenPosition) ->
|
||||
@compositeCursor.addCursorAtScreenPosition(screenPosition)
|
||||
|
||||
addCursorAtBufferPosition: (bufferPosition) ->
|
||||
@compositeCursor.addCursorAtBufferPosition(bufferPosition)
|
||||
|
||||
addSelectionForCursor: (cursor) ->
|
||||
@compositeSelection.addSelectionForCursor(cursor)
|
||||
|
||||
handleEvents: ->
|
||||
@on 'focus', =>
|
||||
@hiddenInput.focus()
|
||||
false
|
||||
|
||||
@hiddenInput.on 'focus', =>
|
||||
@rootView()?.editorFocused(this)
|
||||
@isFocused = true
|
||||
@addClass 'focused'
|
||||
|
||||
@hiddenInput.on 'focusout', =>
|
||||
@isFocused = false
|
||||
@removeClass 'focused'
|
||||
|
||||
@on 'mousedown', '.fold-placeholder', (e) =>
|
||||
@destroyFold($(e.currentTarget).attr('foldId'))
|
||||
false
|
||||
|
||||
@on 'mousedown', (e) =>
|
||||
clickCount = e.originalEvent.detail
|
||||
|
||||
if clickCount == 1
|
||||
screenPosition = @screenPositionFromMouseEvent(e)
|
||||
if e.metaKey
|
||||
@addCursorAtScreenPosition(screenPosition)
|
||||
else
|
||||
@setCursorScreenPosition(screenPosition)
|
||||
else if clickCount == 2
|
||||
@compositeSelection.getLastSelection().selectWord()
|
||||
else if clickCount >= 3
|
||||
@compositeSelection.getLastSelection().selectLine()
|
||||
|
||||
@selectOnMousemoveUntilMouseup()
|
||||
|
||||
@hiddenInput.on "textInput", (e) =>
|
||||
@insertText(e.originalEvent.data)
|
||||
|
||||
@horizontalScroller.on 'scroll', =>
|
||||
if @horizontalScroller.scrollLeft() == 0
|
||||
@gutter.removeClass('drop-shadow')
|
||||
else
|
||||
@gutter.addClass('drop-shadow')
|
||||
|
||||
@one 'attach', =>
|
||||
@calculateDimensions()
|
||||
@hiddenInput.width(@charWidth)
|
||||
@setMaxLineLength() if @softWrap
|
||||
@focus()
|
||||
|
||||
rootView: ->
|
||||
@parents('#root-view').view()
|
||||
|
||||
selectOnMousemoveUntilMouseup: ->
|
||||
moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
|
||||
@on 'mousemove', moveHandler
|
||||
$(document).one 'mouseup', =>
|
||||
@off 'mousemove', moveHandler
|
||||
reverse = @compositeSelection.getLastSelection().isReversed()
|
||||
@compositeSelection.mergeIntersectingSelections({reverse})
|
||||
|
||||
renderLines: ->
|
||||
@lineCache = []
|
||||
@lines.find('.line').remove()
|
||||
@insertLineElements(0, @buildLineElements(0, @getLastScreenRow()))
|
||||
|
||||
getScreenLines: ->
|
||||
@renderer.getLines()
|
||||
|
||||
linesForRows: (start, end) ->
|
||||
@renderer.linesForRows(start, end)
|
||||
|
||||
screenLineCount: ->
|
||||
@renderer.lineCount()
|
||||
|
||||
getLastScreenRow: ->
|
||||
@screenLineCount() - 1
|
||||
|
||||
setBuffer: (buffer) ->
|
||||
if @buffer
|
||||
@saveEditSession()
|
||||
@unsubscribeFromBuffer()
|
||||
|
||||
@buffer = buffer
|
||||
@trigger 'buffer-path-change'
|
||||
@buffer.on "path-change.editor#{@id}", => @trigger 'buffer-path-change'
|
||||
|
||||
@renderer = new Renderer(@buffer)
|
||||
@renderLines()
|
||||
@gutter.renderLineNumbers()
|
||||
|
||||
@loadEditSessionForBuffer(@buffer)
|
||||
|
||||
@buffer.on "change.editor#{@id}", (e) => @handleBufferChange(e)
|
||||
@renderer.on 'change', (e) => @handleRendererChange(e)
|
||||
|
||||
loadEditSessionForBuffer: (buffer) ->
|
||||
@editSession = (@editSessionsByBufferId[buffer.id] ?= new EditSession)
|
||||
@setCursorScreenPosition(@editSession.cursorScreenPosition)
|
||||
@scrollTop(@editSession.scrollTop)
|
||||
@horizontalScroller.scrollLeft(@editSession.scrollLeft)
|
||||
|
||||
saveEditSession: ->
|
||||
@editSession.cursorScreenPosition = @getCursorScreenPosition()
|
||||
@editSession.scrollTop = @scrollTop()
|
||||
@editSession.scrollLeft = @horizontalScroller.scrollLeft()
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
@compositeCursor.handleBufferChange(e)
|
||||
@compositeSelection.handleBufferChange(e)
|
||||
|
||||
handleRendererChange: (e) ->
|
||||
{ oldRange, newRange } = e
|
||||
unless newRange.isSingleLine() and newRange.coversSameRows(oldRange)
|
||||
@gutter.renderLineNumbers(@getScreenLines())
|
||||
|
||||
@compositeCursor.updateBufferPosition() unless e.bufferChanged
|
||||
|
||||
lineElements = @buildLineElements(newRange.start.row, newRange.end.row)
|
||||
@replaceLineElements(oldRange.start.row, oldRange.end.row, lineElements)
|
||||
|
||||
buildLineElements: (startRow, endRow) ->
|
||||
charWidth = @charWidth
|
||||
charHeight = @charHeight
|
||||
lines = @renderer.linesForRows(startRow, endRow)
|
||||
$$ ->
|
||||
for line in lines
|
||||
@div class: 'line', =>
|
||||
appendNbsp = true
|
||||
for token in line.tokens
|
||||
if token.type is 'fold-placeholder'
|
||||
@span ' ', class: 'fold-placeholder', style: "width: #{3 * charWidth}px; height: #{charHeight}px;", 'foldId': token.fold.id, =>
|
||||
@div class: "ellipsis", => @raw "…"
|
||||
else
|
||||
appendNbsp = false
|
||||
@span { class: token.type.replace('.', ' ') }, token.value
|
||||
@raw ' ' if appendNbsp
|
||||
|
||||
insertLineElements: (row, lineElements) ->
|
||||
@spliceLineElements(row, 0, lineElements)
|
||||
|
||||
replaceLineElements: (startRow, endRow, lineElements) ->
|
||||
@spliceLineElements(startRow, endRow - startRow + 1, lineElements)
|
||||
|
||||
spliceLineElements: (startRow, rowCount, lineElements) ->
|
||||
endRow = startRow + rowCount
|
||||
elementToInsertBefore = @lineCache[startRow]
|
||||
elementsToReplace = @lineCache[startRow...endRow]
|
||||
@lineCache[startRow...endRow] = lineElements?.toArray() or []
|
||||
|
||||
lines = @lines[0]
|
||||
if lineElements
|
||||
fragment = document.createDocumentFragment()
|
||||
lineElements.each -> fragment.appendChild(this)
|
||||
if elementToInsertBefore
|
||||
lines.insertBefore(fragment, elementToInsertBefore)
|
||||
else
|
||||
lines.appendChild(fragment)
|
||||
|
||||
elementsToReplace.forEach (element) =>
|
||||
lines.removeChild(element)
|
||||
|
||||
getLineElement: (row) ->
|
||||
@lineCache[row]
|
||||
|
||||
toggleSoftWrap: ->
|
||||
@setSoftWrap(not @softWrap)
|
||||
|
||||
setMaxLineLength: (maxLineLength) ->
|
||||
maxLineLength ?=
|
||||
if @softWrap
|
||||
Math.floor(@horizontalScroller.width() / @charWidth)
|
||||
else
|
||||
Infinity
|
||||
|
||||
@renderer.setMaxLineLength(maxLineLength) if maxLineLength
|
||||
|
||||
createFold: (range) ->
|
||||
@renderer.createFold(range)
|
||||
|
||||
setSoftWrap: (@softWrap, maxLineLength=undefined) ->
|
||||
@setMaxLineLength(maxLineLength)
|
||||
if @softWrap
|
||||
@addClass 'soft-wrap'
|
||||
@_setMaxLineLength = => @setMaxLineLength()
|
||||
$(window).on 'resize', @_setMaxLineLength
|
||||
else
|
||||
@removeClass 'soft-wrap'
|
||||
$(window).off 'resize', @_setMaxLineLength
|
||||
|
||||
save: ->
|
||||
if not @buffer.getPath()
|
||||
path = $native.saveDialog()
|
||||
return if not path
|
||||
@buffer.saveAs(path)
|
||||
else
|
||||
@buffer.save()
|
||||
|
||||
clipScreenPosition: (screenPosition, options={}) ->
|
||||
@renderer.clipScreenPosition(screenPosition, options)
|
||||
|
||||
pixelPositionForScreenPosition: ({row, column}) ->
|
||||
{ top: row * @lineHeight, left: column * @charWidth }
|
||||
|
||||
screenPositionFromPixelPosition: ({top, left}) ->
|
||||
screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth))
|
||||
|
||||
screenPositionForBufferPosition: (position) ->
|
||||
@renderer.screenPositionForBufferPosition(position)
|
||||
|
||||
bufferPositionForScreenPosition: (position) ->
|
||||
@renderer.bufferPositionForScreenPosition(position)
|
||||
|
||||
screenRangeForBufferRange: (range) ->
|
||||
@renderer.screenRangeForBufferRange(range)
|
||||
|
||||
bufferRangeForScreenRange: (range) ->
|
||||
@renderer.bufferRangeForScreenRange(range)
|
||||
|
||||
bufferRowsForScreenRows: ->
|
||||
@renderer.bufferRowsForScreenRows()
|
||||
|
||||
screenPositionFromMouseEvent: (e) ->
|
||||
{ pageX, pageY } = e
|
||||
@screenPositionFromPixelPosition
|
||||
top: pageY - @horizontalScroller.offset().top
|
||||
left: pageX - @horizontalScroller.offset().left + @horizontalScroller.scrollLeft()
|
||||
|
||||
calculateDimensions: ->
|
||||
fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
|
||||
@lines.append(fragment)
|
||||
@charWidth = fragment.width()
|
||||
@charHeight = fragment.find('span').height()
|
||||
@lineHeight = fragment.outerHeight()
|
||||
fragment.remove()
|
||||
|
||||
getCursors: -> @compositeCursor.getCursors()
|
||||
moveCursorUp: -> @compositeCursor.moveUp()
|
||||
moveCursorDown: -> @compositeCursor.moveDown()
|
||||
moveCursorRight: -> @compositeCursor.moveRight()
|
||||
moveCursorLeft: -> @compositeCursor.moveLeft()
|
||||
moveCursorToNextWord: -> @compositeCursor.moveToNextWord()
|
||||
moveCursorToBeginningOfWord: -> @compositeCursor.moveToBeginningOfWord()
|
||||
moveCursorToEndOfWord: -> @compositeCursor.moveToEndOfWord()
|
||||
moveCursorToTop: -> @compositeCursor.moveToTop()
|
||||
moveCursorToBottom: -> @compositeCursor.moveToBottom()
|
||||
moveCursorToBeginningOfLine: -> @compositeCursor.moveToBeginningOfLine()
|
||||
moveCursorToFirstCharacterOfLine: -> @compositeCursor.moveToFirstCharacterOfLine()
|
||||
moveCursorToEndOfLine: -> @compositeCursor.moveToEndOfLine()
|
||||
setCursorScreenPosition: (position) -> @compositeCursor.setScreenPosition(position)
|
||||
getCursorScreenPosition: -> @compositeCursor.getCursor().getScreenPosition()
|
||||
setCursorBufferPosition: (position) -> @compositeCursor.setBufferPosition(position)
|
||||
getCursorBufferPosition: -> @compositeCursor.getCursor().getBufferPosition()
|
||||
|
||||
getSelection: (index) -> @compositeSelection.getSelection(index)
|
||||
getSelections: -> @compositeSelection.getSelections()
|
||||
getLastSelectionInBuffer: -> @compositeSelection.getLastSelectionInBuffer()
|
||||
getSelectedText: -> @compositeSelection.getSelection().getText()
|
||||
setSelectionBufferRange: (bufferRange, options) -> @compositeSelection.setBufferRange(bufferRange, options)
|
||||
addSelectionForBufferRange: (bufferRange, options) -> @compositeSelection.addSelectionForBufferRange(bufferRange, options)
|
||||
selectRight: -> @compositeSelection.selectRight()
|
||||
selectLeft: -> @compositeSelection.selectLeft()
|
||||
selectUp: -> @compositeSelection.selectUp()
|
||||
selectDown: -> @compositeSelection.selectDown()
|
||||
selectToTop: -> @compositeSelection.selectToTop()
|
||||
selectToBottom: -> @compositeSelection.selectToBottom()
|
||||
selectToBeginningOfLine: -> @compositeSelection.selectToBeginningOfLine()
|
||||
selectToEndOfLine: -> @compositeSelection.selectToEndOfLine()
|
||||
selectToBeginningOfWord: -> @compositeSelection.selectToBeginningOfWord()
|
||||
selectToEndOfWord: -> @compositeSelection.selectToEndOfWord()
|
||||
selectToScreenPosition: (position) -> @compositeSelection.selectToScreenPosition(position)
|
||||
clearSelections: -> @compositeSelection.clearSelections()
|
||||
backspace: -> @compositeSelection.backspace()
|
||||
backspaceToBeginningOfWord: -> @compositeSelection.backspaceToBeginningOfWord()
|
||||
delete: -> @compositeSelection.delete()
|
||||
deleteToEndOfWord: -> @compositeSelection.deleteToEndOfWord()
|
||||
|
||||
setText: (text) -> @buffer.setText(text)
|
||||
getText: -> @buffer.getText()
|
||||
getLastBufferRow: -> @buffer.getLastRow()
|
||||
getTextInRange: (range) -> @buffer.getTextInRange(range)
|
||||
getEofPosition: -> @buffer.getEofPosition()
|
||||
lineForBufferRow: (row) -> @buffer.lineForRow(row)
|
||||
lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row)
|
||||
rangeForBufferRow: (row) -> @buffer.rangeForRow(row)
|
||||
traverseRegexMatchesInRange: (args...) -> @buffer.traverseRegexMatchesInRange(args...)
|
||||
backwardsTraverseRegexMatchesInRange: (args...) -> @buffer.backwardsTraverseRegexMatchesInRange(args...)
|
||||
|
||||
insertText: (text) ->
|
||||
@compositeSelection.insertText(text)
|
||||
|
||||
cutSelection: -> @compositeSelection.cut()
|
||||
copySelection: -> @compositeSelection.copy()
|
||||
paste: -> @insertText($native.readFromPasteboard())
|
||||
|
||||
foldSelection: -> @getSelection().fold()
|
||||
|
||||
|
||||
undo: ->
|
||||
@buffer.undo()
|
||||
|
||||
redo: ->
|
||||
@buffer.redo()
|
||||
|
||||
destroyFold: (foldId) ->
|
||||
fold = @renderer.foldsById[foldId]
|
||||
fold.destroy()
|
||||
@setCursorBufferPosition(fold.start)
|
||||
|
||||
splitLeft: ->
|
||||
@split('row', 'before')
|
||||
|
||||
splitRight: ->
|
||||
@split('row', 'after')
|
||||
|
||||
splitUp: ->
|
||||
@split('column', 'before')
|
||||
|
||||
splitDown: ->
|
||||
@split('column', 'after')
|
||||
|
||||
split: (axis, insertMethod) ->
|
||||
unless @parent().hasClass axis
|
||||
container = $$ -> @div class: axis
|
||||
container.insertBefore(this).append(this.detach())
|
||||
|
||||
editor = new Editor({@buffer})
|
||||
editor.setCursorScreenPosition(@getCursorScreenPosition())
|
||||
|
||||
this[insertMethod](editor)
|
||||
@rootView().adjustSplitPanes()
|
||||
editor
|
||||
|
||||
remove: (selector, keepData) ->
|
||||
return super if keepData
|
||||
@unsubscribeFromBuffer()
|
||||
rootView = @rootView()
|
||||
parent = @parent()
|
||||
super
|
||||
parent.remove() if parent.is('.row:empty, .column:empty')
|
||||
rootView?.editorRemoved(this)
|
||||
|
||||
unsubscribeFromBuffer: ->
|
||||
@buffer.off ".editor#{@id}"
|
||||
@renderer.destroy()
|
||||
|
||||
stateForScreenRow: (row) ->
|
||||
@renderer.lineForRow(row).state
|
||||
|
||||
getCurrentMode: ->
|
||||
@buffer.getMode()
|
||||
|
||||
logLines: ->
|
||||
@renderer.logLines()
|
||||
51
src/app/event-emitter.coffee
Normal file
51
src/app/event-emitter.coffee
Normal file
@@ -0,0 +1,51 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
on: (eventName, handler) ->
|
||||
[eventName, namespace] = eventName.split('.')
|
||||
|
||||
@eventHandlersByEventName ?= {}
|
||||
@eventHandlersByEventName[eventName] ?= []
|
||||
@eventHandlersByEventName[eventName].push(handler)
|
||||
|
||||
if namespace
|
||||
@eventHandlersByNamespace ?= {}
|
||||
@eventHandlersByNamespace[namespace] ?= {}
|
||||
@eventHandlersByNamespace[namespace][eventName] ?= []
|
||||
@eventHandlersByNamespace[namespace][eventName].push(handler)
|
||||
|
||||
trigger: (eventName, event) ->
|
||||
[eventName, namespace] = eventName.split('.')
|
||||
|
||||
if namespace
|
||||
@eventHandlersByNamespace?[namespace]?[eventName]?.forEach (handler) -> handler(event)
|
||||
else
|
||||
@eventHandlersByEventName?[eventName]?.forEach (handler) -> handler(event)
|
||||
|
||||
off: (eventName, handler) ->
|
||||
[eventName, namespace] = eventName.split('.')
|
||||
eventName = undefined if eventName is ''
|
||||
|
||||
if namespace
|
||||
if eventName
|
||||
handlers = @eventHandlersByNamespace?[namespace]?[eventName] ? []
|
||||
for handler in new Array(handlers...)
|
||||
_.remove(handlers, handler)
|
||||
@off eventName, handler
|
||||
else
|
||||
for eventName, handlers of @eventHandlersByNamespace?[namespace] ? {}
|
||||
for handler in new Array(handlers...)
|
||||
_.remove(handlers, handler)
|
||||
@off eventName, handler
|
||||
else
|
||||
if handler
|
||||
_.remove(@eventHandlersByEventName[eventName], handler)
|
||||
else
|
||||
delete @eventHandlersByEventName?[eventName]
|
||||
|
||||
subscriptionCount: ->
|
||||
count = 0
|
||||
for name, handlers of @eventHandlersByEventName
|
||||
count += handlers.length
|
||||
count
|
||||
|
||||
78
src/app/file-finder.coffee
Normal file
78
src/app/file-finder.coffee
Normal file
@@ -0,0 +1,78 @@
|
||||
$ = require 'jquery'
|
||||
{View} = require 'space-pen'
|
||||
stringScore = require 'stringscore'
|
||||
Editor = require 'editor'
|
||||
|
||||
module.exports =
|
||||
class FileFinder extends View
|
||||
@content: ->
|
||||
@div class: 'file-finder', =>
|
||||
@ol outlet: 'pathList'
|
||||
@subview 'editor', new Editor
|
||||
|
||||
paths: null
|
||||
maxResults: null
|
||||
|
||||
initialize: ({@paths, @selected}) ->
|
||||
requireStylesheet 'file-finder.css'
|
||||
@maxResults = 10
|
||||
@previousFocusedElement = $(document.activeElement)
|
||||
|
||||
@populatePathList()
|
||||
|
||||
@on 'file-finder:close', => @remove()
|
||||
@on 'move-up', => @moveUp()
|
||||
@on 'move-down', => @moveDown()
|
||||
@on 'file-finder:select-file', => @select()
|
||||
|
||||
@editor.addClass 'single-line'
|
||||
@editor.buffer.on 'change', => @populatePathList()
|
||||
@editor.off 'move-up move-down'
|
||||
|
||||
populatePathList: ->
|
||||
@pathList.empty()
|
||||
for path in @findMatches(@editor.buffer.getText())
|
||||
@pathList.append $("<li>#{path}</li>")
|
||||
|
||||
@pathList.children('li:first').addClass 'selected'
|
||||
|
||||
findSelectedLi: ->
|
||||
@pathList.children('li.selected')
|
||||
|
||||
select: ->
|
||||
filePath = @findSelectedLi().text()
|
||||
@selected(filePath) if filePath and @selected
|
||||
@remove()
|
||||
|
||||
remove: ->
|
||||
super()
|
||||
@previousFocusedElement.focus()
|
||||
|
||||
moveUp: ->
|
||||
@findSelectedLi()
|
||||
.filter(':not(:first-child)')
|
||||
.removeClass('selected')
|
||||
.prev()
|
||||
.addClass('selected')
|
||||
|
||||
moveDown: ->
|
||||
@findSelectedLi()
|
||||
.filter(':not(:last-child)')
|
||||
.removeClass('selected')
|
||||
.next()
|
||||
.addClass('selected')
|
||||
|
||||
findMatches: (query) ->
|
||||
if not query
|
||||
paths = @paths
|
||||
else
|
||||
scoredPaths = ({path, score: stringScore(path, query)} for path in @paths)
|
||||
scoredPaths.sort (a, b) ->
|
||||
if a.score > b.score then -1
|
||||
else if a.score < b.score then 1
|
||||
else 0
|
||||
window.x = scoredPaths
|
||||
|
||||
paths = (pathAndScore.path for pathAndScore in scoredPaths when pathAndScore.score > 0)
|
||||
|
||||
paths.slice 0, @maxResults
|
||||
51
src/app/fold.coffee
Normal file
51
src/app/fold.coffee
Normal file
@@ -0,0 +1,51 @@
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class Fold
|
||||
@idCounter: 1
|
||||
start: null
|
||||
end: null
|
||||
|
||||
constructor: (@lineFolder, {@start, @end}) ->
|
||||
@id = @constructor.idCounter++
|
||||
|
||||
destroy: ->
|
||||
@lineFolder.destroyFold(this)
|
||||
|
||||
getRange: ->
|
||||
new Range(@start, @end)
|
||||
|
||||
handleBufferChange: (event) ->
|
||||
oldStartRow = @start.row
|
||||
|
||||
{ oldRange } = event
|
||||
if oldRange.start.isLessThanOrEqual(@start) and oldRange.end.isGreaterThanOrEqual(@end)
|
||||
@lineFolder.unregisterFold(oldStartRow, this)
|
||||
return
|
||||
|
||||
changeInsideFold = @start.isLessThanOrEqual(oldRange.start) and @end.isGreaterThan(oldRange.end)
|
||||
|
||||
@start = @updateAnchorPoint(@start, event)
|
||||
@end = @updateAnchorPoint(@end, event, false)
|
||||
|
||||
if @start.row != oldStartRow
|
||||
@lineFolder.unregisterFold(oldStartRow, this)
|
||||
@lineFolder.registerFold(@start.row, this)
|
||||
|
||||
changeInsideFold
|
||||
|
||||
updateAnchorPoint: (point, event, inclusive=true) ->
|
||||
{ newRange, oldRange } = event
|
||||
if inclusive
|
||||
return point if oldRange.end.isGreaterThan(point)
|
||||
else
|
||||
return point if oldRange.end.isGreaterThanOrEqual(point)
|
||||
|
||||
newRange.end.add(point.subtract(oldRange.end))
|
||||
|
||||
compare: (other) ->
|
||||
startComparison = @start.compare(other.start)
|
||||
if startComparison == 0
|
||||
other.end.compare(@end)
|
||||
else
|
||||
startComparison
|
||||
17
src/app/gutter.coffee
Normal file
17
src/app/gutter.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
{View, $$$} = require 'space-pen'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Gutter extends View
|
||||
@content: ->
|
||||
@div class: 'gutter'
|
||||
|
||||
renderLineNumbers: ->
|
||||
lastRow = -1
|
||||
rows = @parentView.bufferRowsForScreenRows()
|
||||
this[0].innerHTML = $$$ ->
|
||||
for row in rows
|
||||
@div {class: 'line-number'}, if row == lastRow then '•' else row + 1
|
||||
lastRow = row
|
||||
72
src/app/highlighter.coffee
Normal file
72
src/app/highlighter.coffee
Normal file
@@ -0,0 +1,72 @@
|
||||
_ = require 'underscore'
|
||||
ScreenLineFragment = require 'screen-line-fragment'
|
||||
EventEmitter = require 'event-emitter'
|
||||
|
||||
module.exports =
|
||||
class Highlighter
|
||||
@idCounter: 1
|
||||
buffer: null
|
||||
screenLines: []
|
||||
|
||||
constructor: (@buffer) ->
|
||||
@id = @constructor.idCounter++
|
||||
@screenLines = @buildLinesForScreenRows('start', 0, @buffer.getLastRow())
|
||||
@buffer.on "change.highlighter#{@id}", (e) => @handleBufferChange(e)
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
oldRange = e.oldRange.copy()
|
||||
newRange = e.newRange.copy()
|
||||
previousState = @screenLines[oldRange.end.row].state # used in spill detection below
|
||||
|
||||
startState = @screenLines[newRange.start.row - 1]?.state or 'start'
|
||||
@screenLines[oldRange.start.row..oldRange.end.row] =
|
||||
@buildLinesForScreenRows(startState, newRange.start.row, newRange.end.row)
|
||||
|
||||
# spill detection
|
||||
# compare scanner state of last re-highlighted line with its previous state.
|
||||
# if it differs, re-tokenize the next line with the new state and repeat for
|
||||
# each line until the line's new state matches the previous state. this covers
|
||||
# cases like inserting a /* needing to comment out lines below until we see a */
|
||||
for row in [newRange.end.row...@buffer.getLastRow()]
|
||||
break if @screenLines[row].state == previousState
|
||||
nextRow = row + 1
|
||||
previousState = @screenLines[nextRow].state
|
||||
@screenLines[nextRow] = @buildLineForScreenRow(@screenLines[row].state, nextRow)
|
||||
|
||||
# if highlighting spilled beyond the bounds of the textual change, update
|
||||
# the pre and post range to reflect area of highlight changes
|
||||
if nextRow > newRange.end.row
|
||||
oldRange.end.row += (nextRow - newRange.end.row)
|
||||
newRange.end.row = nextRow
|
||||
endColumn = @buffer.lineForRow(nextRow).length
|
||||
newRange.end.column = endColumn
|
||||
oldRange.end.column = endColumn
|
||||
|
||||
@trigger("change", {oldRange, newRange})
|
||||
|
||||
buildLinesForScreenRows: (startState, startRow, endRow) ->
|
||||
state = startState
|
||||
for row in [startRow..endRow]
|
||||
screenLine = @buildLineForScreenRow(state, row)
|
||||
state = screenLine.state
|
||||
screenLine
|
||||
|
||||
buildLineForScreenRow: (state, row) ->
|
||||
tokenizer = @buffer.getMode().getTokenizer()
|
||||
line = @buffer.lineForRow(row)
|
||||
{tokens, state} = tokenizer.getLineTokens(line, state)
|
||||
new ScreenLineFragment(tokens, line, [1, 0], [1, 0], { state })
|
||||
|
||||
lineForScreenRow: (row) ->
|
||||
@screenLines[row]
|
||||
|
||||
lineForRow: (row) ->
|
||||
@lineForScreenRow(row)
|
||||
|
||||
linesForScreenRows: (startRow, endRow) ->
|
||||
@screenLines[startRow..endRow]
|
||||
|
||||
destroy: ->
|
||||
@buffer.off ".highlighter#{@id}"
|
||||
|
||||
_.extend(Highlighter.prototype, EventEmitter)
|
||||
94
src/app/keymap.coffee
Normal file
94
src/app/keymap.coffee
Normal file
@@ -0,0 +1,94 @@
|
||||
fs = require 'fs'
|
||||
BindingSet = require 'binding-set'
|
||||
Specificity = require 'specificity'
|
||||
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class Keymap
|
||||
bindingSetsBySelector: null
|
||||
|
||||
constructor: ->
|
||||
@bindingSets = []
|
||||
|
||||
bindDefaultKeys: ->
|
||||
@bindKeys "*",
|
||||
'meta-n': 'new-window'
|
||||
'meta-,': 'open-user-configuration'
|
||||
'meta-o': 'open'
|
||||
|
||||
$(document).on 'new-window', => $native.newWindow()
|
||||
$(document).on 'open-user-configuration', => atom.open(atom.userConfigurationPath)
|
||||
$(document).on 'open', =>
|
||||
path = $native.openDialog()
|
||||
atom.open(path) if path
|
||||
|
||||
unbindDefaultKeys: ->
|
||||
$(document).unbind 'new-window', @_newWindow
|
||||
$(document).unbind 'open', @_open
|
||||
|
||||
bindKeys: (selector, bindings) ->
|
||||
@bindingSets.unshift(new BindingSet(selector, bindings))
|
||||
|
||||
bindKey: (selector, pattern, eventName) ->
|
||||
bindings = {}
|
||||
bindings[pattern] = eventName
|
||||
@bindKeys(selector, bindings)
|
||||
|
||||
handleKeyEvent: (event) ->
|
||||
event.keystroke = @keystrokeStringForEvent(event)
|
||||
currentNode = $(event.target)
|
||||
while currentNode.length
|
||||
candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
|
||||
candidateBindingSets.sort (a, b) -> b.specificity - a.specificity
|
||||
for bindingSet in candidateBindingSets
|
||||
command = bindingSet.commandForEvent(event)
|
||||
if command
|
||||
@triggerCommandEvent(event, command)
|
||||
return false
|
||||
else if command == false
|
||||
return false
|
||||
currentNode = currentNode.parent()
|
||||
true
|
||||
|
||||
reset: ->
|
||||
@bindingSets = []
|
||||
|
||||
triggerCommandEvent: (keyEvent, commandName) ->
|
||||
commandEvent = $.Event(commandName)
|
||||
commandEvent.keyEvent = keyEvent
|
||||
$(keyEvent.target).trigger(commandEvent)
|
||||
|
||||
keystrokeStringForEvent: (event) ->
|
||||
if /^U\+/i.test event.originalEvent.keyIdentifier
|
||||
hexCharCode = event.originalEvent.keyIdentifier.replace(/^U\+/i, '')
|
||||
charCode = parseInt(hexCharCode, 16)
|
||||
key = @keyFromCharCode(charCode)
|
||||
else
|
||||
key = event.originalEvent.keyIdentifier.toLowerCase()
|
||||
|
||||
modifiers = ''
|
||||
if event.altKey and key isnt 'alt'
|
||||
modifiers += 'alt-'
|
||||
if event.ctrlKey and key isnt 'ctrl'
|
||||
modifiers += 'ctrl-'
|
||||
if event.metaKey and key isnt 'meta'
|
||||
modifiers += 'meta-'
|
||||
|
||||
if event.shiftKey
|
||||
isNamedKey = key.length > 1
|
||||
modifiers += 'shift-' if isNamedKey
|
||||
else
|
||||
key = key.toLowerCase()
|
||||
|
||||
"#{modifiers}#{key}"
|
||||
|
||||
keyFromCharCode: (charCode) ->
|
||||
switch charCode
|
||||
when 8 then 'backspace'
|
||||
when 9 then 'tab'
|
||||
when 13 then 'enter'
|
||||
when 27 then 'escape'
|
||||
when 32 then 'space'
|
||||
when 127 then 'delete'
|
||||
else String.fromCharCode(charCode)
|
||||
15
src/app/keymaps/apple.coffee
Normal file
15
src/app/keymaps/apple.coffee
Normal file
@@ -0,0 +1,15 @@
|
||||
window.keymap.bindKeys '.editor'
|
||||
'meta-up': 'move-to-top'
|
||||
'meta-down': 'move-to-bottom'
|
||||
'meta-right': 'move-to-end-of-line'
|
||||
'meta-left': 'move-to-beginning-of-line'
|
||||
'alt-left': 'move-to-beginning-of-word'
|
||||
'alt-right': 'move-to-end-of-word'
|
||||
'meta-shift-up': 'select-to-top'
|
||||
'meta-shift-down': 'select-to-bottom'
|
||||
'meta-shift-left': 'select-to-beginning-of-line'
|
||||
'meta-shift-right': 'select-to-end-of-line'
|
||||
'alt-shift-left': 'select-to-beginning-of-word'
|
||||
'alt-shift-right': 'select-to-end-of-word'
|
||||
'alt-backspace': 'backspace-to-beginning-of-word'
|
||||
'alt-delete': 'delete-to-end-of-word'
|
||||
9
src/app/keymaps/command-panel.coffee
Normal file
9
src/app/keymaps/command-panel.coffee
Normal file
@@ -0,0 +1,9 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'meta-:': 'command-panel:toggle'
|
||||
|
||||
window.keymap.bindKeys '.command-panel .editor',
|
||||
escape: 'command-panel:toggle'
|
||||
enter: 'command-panel:execute'
|
||||
|
||||
window.keymap.bindKeys '.editor',
|
||||
'meta-g': 'command-panel:repeat-relative-address'
|
||||
30
src/app/keymaps/editor.coffee
Normal file
30
src/app/keymaps/editor.coffee
Normal file
@@ -0,0 +1,30 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'meta-s': 'save'
|
||||
'meta-w': 'close'
|
||||
'alt-meta-i': 'show-console'
|
||||
'meta-f': 'find-in-file'
|
||||
|
||||
window.keymap.bindKeys '.editor',
|
||||
'meta-s': 'save'
|
||||
right: 'move-right'
|
||||
left: 'move-left'
|
||||
down: 'move-down'
|
||||
up: 'move-up'
|
||||
'shift-right': 'select-right'
|
||||
'shift-left': 'select-left'
|
||||
'shift-up': 'select-up'
|
||||
'shift-down': 'select-down'
|
||||
enter: 'newline'
|
||||
backspace: 'backspace'
|
||||
'delete': 'delete'
|
||||
'meta-x': 'cut'
|
||||
'meta-c': 'copy'
|
||||
'meta-v': 'paste'
|
||||
'meta-z': 'undo'
|
||||
'meta-Z': 'redo'
|
||||
'alt-meta-w': 'toggle-soft-wrap'
|
||||
'alt-meta-f': 'fold-selection'
|
||||
'alt-meta-left': 'split-left'
|
||||
'alt-meta-right': 'split-right'
|
||||
'alt-meta-up': 'split-up'
|
||||
'alt-meta-down': 'split-down'
|
||||
13
src/app/keymaps/emacs.coffee
Normal file
13
src/app/keymaps/emacs.coffee
Normal file
@@ -0,0 +1,13 @@
|
||||
window.keymap.bindKeys '.editor',
|
||||
'ctrl-f': 'move-right'
|
||||
'ctrl-b': 'move-left'
|
||||
'ctrl-p': 'move-up'
|
||||
'ctrl-n': 'move-down'
|
||||
'alt-f': 'move-to-end-of-word'
|
||||
'alt-b': 'move-to-beginning-of-word'
|
||||
'ctrl-a': 'move-to-first-character-of-line'
|
||||
'ctrl-e': 'move-to-end-of-line'
|
||||
'ctrl-h': 'backspace'
|
||||
'ctrl-d': 'delete'
|
||||
'alt-h': 'backspace-to-beginning-of-word'
|
||||
'alt-d': 'delete-to-end-of-word'
|
||||
6
src/app/keymaps/file-finder.coffee
Normal file
6
src/app/keymaps/file-finder.coffee
Normal file
@@ -0,0 +1,6 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'meta-t': 'toggle-file-finder'
|
||||
|
||||
window.keymap.bindKeys ".file-finder .editor",
|
||||
'enter': 'file-finder:select-file',
|
||||
'escape': 'file-finder:close'
|
||||
176
src/app/line-map.coffee
Normal file
176
src/app/line-map.coffee
Normal file
@@ -0,0 +1,176 @@
|
||||
_ = require 'underscore'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class LineMap
|
||||
constructor: ->
|
||||
@lineFragments = []
|
||||
|
||||
insertAtBufferRow: (bufferRow, lineFragments) ->
|
||||
@spliceAtBufferRow(bufferRow, 0, lineFragments)
|
||||
|
||||
spliceAtBufferRow: (startRow, rowCount, lineFragments) ->
|
||||
@spliceByDelta('bufferDelta', startRow, rowCount, lineFragments)
|
||||
|
||||
spliceAtScreenRow: (startRow, rowCount, lineFragments) ->
|
||||
@spliceByDelta('screenDelta', startRow, rowCount, lineFragments)
|
||||
|
||||
replaceBufferRows: (start, end, lineFragments) ->
|
||||
@spliceAtBufferRow(start, end - start + 1, lineFragments)
|
||||
|
||||
replaceScreenRows: (start, end, lineFragments) ->
|
||||
@spliceAtScreenRow(start, end - start + 1, lineFragments)
|
||||
|
||||
lineForScreenRow: (row) ->
|
||||
@linesForScreenRows(row, row)[0]
|
||||
|
||||
linesForScreenRows: (startRow, endRow) ->
|
||||
@linesByDelta('screenDelta', startRow, endRow)
|
||||
|
||||
lineForBufferRow: (row) ->
|
||||
@linesForBufferRows(row, row)[0]
|
||||
|
||||
linesForBufferRows: (startRow, endRow) ->
|
||||
@linesByDelta('bufferDelta', startRow, endRow)
|
||||
|
||||
bufferRowsForScreenRows: (startRow, endRow=@lastScreenRow())->
|
||||
bufferRows = []
|
||||
currentScreenRow = -1
|
||||
@traverseByDelta 'screenDelta', [startRow, 0], [endRow, 0], ({ screenDelta, bufferDelta }) ->
|
||||
bufferRows.push(bufferDelta.row) if screenDelta.row > currentScreenRow
|
||||
currentScreenRow = screenDelta.row
|
||||
bufferRows
|
||||
|
||||
bufferLineCount: ->
|
||||
@lineCountByDelta('bufferDelta')
|
||||
|
||||
screenLineCount: ->
|
||||
@lineCountByDelta('screenDelta')
|
||||
|
||||
lineCountByDelta: (deltaType) ->
|
||||
@traverseByDelta(deltaType, new Point(Infinity, 0))[deltaType].row
|
||||
|
||||
lastScreenRow: ->
|
||||
@screenLineCount() - 1
|
||||
|
||||
screenPositionForBufferPosition: (bufferPosition) ->
|
||||
@translatePosition('bufferDelta', 'screenDelta', bufferPosition)
|
||||
|
||||
bufferPositionForScreenPosition: (screenPosition) ->
|
||||
@translatePosition('screenDelta', 'bufferDelta', screenPosition)
|
||||
|
||||
screenRangeForBufferRange: (bufferRange) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
start = @screenPositionForBufferPosition(bufferRange.start)
|
||||
end = @screenPositionForBufferPosition(bufferRange.end)
|
||||
new Range(start, end)
|
||||
|
||||
bufferRangeForScreenRange: (screenRange) ->
|
||||
start = @bufferPositionForScreenPosition(screenRange.start)
|
||||
end = @bufferPositionForScreenPosition(screenRange.end)
|
||||
new Range(start, end)
|
||||
|
||||
clipScreenPosition: (screenPosition, options) ->
|
||||
@clipPosition('screenDelta', screenPosition, options)
|
||||
|
||||
clipPosition: (deltaType, position, options={}) ->
|
||||
options.clipToBounds = true
|
||||
@translatePosition(deltaType, deltaType, position, options)
|
||||
|
||||
spliceByDelta: (deltaType, startRow, rowCount, lineFragments) ->
|
||||
stopRow = startRow + rowCount
|
||||
startIndex = undefined
|
||||
stopIndex = 0
|
||||
|
||||
delta = new Point
|
||||
for lineFragment, i in @lineFragments
|
||||
startIndex ?= i if delta.row == startRow
|
||||
break if delta.row == stopRow
|
||||
delta = delta.add(lineFragment[deltaType])
|
||||
stopIndex++
|
||||
startIndex ?= i
|
||||
|
||||
@lineFragments[startIndex...stopIndex] = lineFragments
|
||||
|
||||
linesByDelta: (deltaType, startRow, endRow) ->
|
||||
lines = []
|
||||
pendingFragment = null
|
||||
@traverseByDelta deltaType, new Point(startRow, 0), new Point(endRow, Infinity), ({lineFragment}) ->
|
||||
if pendingFragment
|
||||
pendingFragment = pendingFragment.concat(lineFragment)
|
||||
else
|
||||
pendingFragment = lineFragment
|
||||
if pendingFragment[deltaType].row > 0
|
||||
lines.push pendingFragment
|
||||
pendingFragment = null
|
||||
lines
|
||||
|
||||
translatePosition: (sourceDeltaType, targetDeltaType, sourcePosition, options={}) ->
|
||||
sourcePosition = Point.fromObject(sourcePosition)
|
||||
wrapBeyondNewlines = options.wrapBeyondNewlines ? false
|
||||
wrapAtSoftNewlines = options.wrapAtSoftNewlines ? false
|
||||
skipAtomicTokens = options.skipAtomicTokens ? false
|
||||
clipToBounds = options.clipToBounds ? false
|
||||
|
||||
@clipToBounds(sourceDeltaType, sourcePosition) if clipToBounds
|
||||
traversalResult = @traverseByDelta(sourceDeltaType, sourcePosition)
|
||||
lastLineFragment = traversalResult.lastLineFragment
|
||||
sourceDelta = traversalResult[sourceDeltaType]
|
||||
targetDelta = traversalResult[targetDeltaType]
|
||||
|
||||
return targetDelta unless lastLineFragment
|
||||
maxSourceColumn = sourceDelta.column + lastLineFragment.text.length
|
||||
maxTargetColumn = targetDelta.column + lastLineFragment.text.length
|
||||
|
||||
if lastLineFragment.isSoftWrapped() and sourcePosition.column >= maxSourceColumn
|
||||
if wrapAtSoftNewlines
|
||||
targetDelta.row++
|
||||
targetDelta.column = 0
|
||||
else
|
||||
targetDelta.column = maxTargetColumn - 1
|
||||
return @clipPosition(targetDeltaType, targetDelta)
|
||||
else if sourcePosition.column > maxSourceColumn and wrapBeyondNewlines
|
||||
targetDelta.row++
|
||||
targetDelta.column = 0
|
||||
else if lastLineFragment.isAtomic
|
||||
if skipAtomicTokens and sourcePosition.column > sourceDelta.column
|
||||
targetDelta.column += lastLineFragment.text.length
|
||||
else
|
||||
additionalColumns = sourcePosition.column - sourceDelta.column
|
||||
targetDelta.column = Math.min(maxTargetColumn, targetDelta.column + additionalColumns)
|
||||
|
||||
targetDelta
|
||||
|
||||
clipToBounds: (deltaType, position) ->
|
||||
if position.column < 0
|
||||
position.column = 0
|
||||
|
||||
if position.row < 0
|
||||
position.row = 0
|
||||
position.column = 0
|
||||
|
||||
maxSourceRow = @lineCountByDelta(deltaType) - 1
|
||||
if position.row > maxSourceRow
|
||||
position.row = maxSourceRow
|
||||
position.column = Infinity
|
||||
|
||||
traverseByDelta: (deltaType, startPosition, endPosition=startPosition, iterator=null) ->
|
||||
traversalDelta = new Point
|
||||
screenDelta = new Point
|
||||
bufferDelta = new Point
|
||||
|
||||
for lineFragment in @lineFragments
|
||||
iterator({ lineFragment, screenDelta, bufferDelta }) if traversalDelta.isGreaterThanOrEqual(startPosition) and iterator?
|
||||
traversalDelta = traversalDelta.add(lineFragment[deltaType])
|
||||
break if traversalDelta.isGreaterThan(endPosition)
|
||||
screenDelta = screenDelta.add(lineFragment.screenDelta)
|
||||
bufferDelta = bufferDelta.add(lineFragment.bufferDelta)
|
||||
|
||||
{ screenDelta, bufferDelta, lastLineFragment: lineFragment }
|
||||
|
||||
logLines: (start=0, end=@screenLineCount() - 1)->
|
||||
for row in [start..end]
|
||||
line = @lineForScreenRow(row).text
|
||||
console.log row, line, line.length
|
||||
|
||||
78
src/app/point.coffee
Normal file
78
src/app/point.coffee
Normal file
@@ -0,0 +1,78 @@
|
||||
module.exports =
|
||||
class Point
|
||||
@fromObject: (object) ->
|
||||
if object instanceof Point
|
||||
object
|
||||
else
|
||||
if object instanceof Array
|
||||
[row, column] = object
|
||||
else
|
||||
{ row, column } = object
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
constructor: (@row=0, @column=0) ->
|
||||
|
||||
copy: ->
|
||||
new Point(@row, @column)
|
||||
|
||||
add: (other) ->
|
||||
row = @row + other.row
|
||||
if other.row == 0
|
||||
column = @column + other.column
|
||||
else
|
||||
column = other.column
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
subtract: (other) ->
|
||||
row = @row - other.row
|
||||
if @row == other.row
|
||||
column = @column - other.column
|
||||
else
|
||||
column = @column
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
splitAt: (column) ->
|
||||
if @row == 0
|
||||
rightColumn = @column - column
|
||||
else
|
||||
rightColumn = @column
|
||||
|
||||
[new Point(0, column), new Point(@row, rightColumn)]
|
||||
|
||||
compare: (other) ->
|
||||
if @row > other.row
|
||||
1
|
||||
else if @row < other.row
|
||||
-1
|
||||
else
|
||||
if @column > other.column
|
||||
1
|
||||
else if @column < other.column
|
||||
-1
|
||||
else
|
||||
0
|
||||
|
||||
isEqual: (other) ->
|
||||
other = Point.fromObject(other)
|
||||
@compare(other) == 0
|
||||
|
||||
isLessThan: (other) ->
|
||||
@compare(other) < 0
|
||||
|
||||
isLessThanOrEqual: (other) ->
|
||||
@compare(other) <= 0
|
||||
|
||||
isGreaterThan: (other) ->
|
||||
@compare(other) > 0
|
||||
|
||||
isGreaterThanOrEqual: (other) ->
|
||||
@compare(other) >= 0
|
||||
|
||||
inspect: ->
|
||||
"(#{@row}, #{@column})"
|
||||
|
||||
toString: ->
|
||||
"#{@row},#{@column}"
|
||||
24
src/app/project.coffee
Normal file
24
src/app/project.coffee
Normal file
@@ -0,0 +1,24 @@
|
||||
fs = require 'fs'
|
||||
Buffer = require 'buffer'
|
||||
|
||||
module.exports =
|
||||
|
||||
class Project
|
||||
buffers: null
|
||||
|
||||
constructor: (@path) ->
|
||||
@buffers = {}
|
||||
|
||||
getFilePaths: ->
|
||||
projectPath = @path
|
||||
fs.async.listTree(@path).pipe (paths) ->
|
||||
path.replace(projectPath, "") for path in paths when fs.isFile(path)
|
||||
|
||||
open: (filePath) ->
|
||||
filePath = @resolve filePath
|
||||
@buffers[filePath] ?= new Buffer(filePath)
|
||||
|
||||
resolve: (filePath) ->
|
||||
filePath = fs.join(@path, filePath) unless filePath[0] == '/'
|
||||
fs.absolute filePath
|
||||
|
||||
64
src/app/range.coffee
Normal file
64
src/app/range.coffee
Normal file
@@ -0,0 +1,64 @@
|
||||
Point = require 'point'
|
||||
_ = require 'underscore'
|
||||
|
||||
|
||||
module.exports =
|
||||
class Range
|
||||
@fromObject: (object) ->
|
||||
if _.isArray(object)
|
||||
new Range(object...)
|
||||
else
|
||||
object
|
||||
|
||||
|
||||
constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) ->
|
||||
pointA = Point.fromObject(pointA)
|
||||
pointB = Point.fromObject(pointB)
|
||||
|
||||
if pointA.compare(pointB) <= 0
|
||||
@start = pointA
|
||||
@end = pointB
|
||||
else
|
||||
@start = pointB
|
||||
@end = pointA
|
||||
|
||||
copy: (range) ->
|
||||
new Range(@start.copy(), @end.copy())
|
||||
|
||||
isEqual: (other) ->
|
||||
if other instanceof Array and other.length == 2
|
||||
other = new Range(other...)
|
||||
|
||||
other.start.isEqual(@start) and other.end.isEqual(@end)
|
||||
|
||||
isSingleLine: ->
|
||||
@start.row == @end.row
|
||||
|
||||
coversSameRows: (other) ->
|
||||
@start.row == other.start.row && @end.row == other.end.row
|
||||
|
||||
inspect: ->
|
||||
"[#{@start.inspect()} - #{@end.inspect()}]"
|
||||
|
||||
intersectsWith: (otherRange) ->
|
||||
if @start.isLessThanOrEqual(otherRange.start)
|
||||
@end.isGreaterThanOrEqual(otherRange.start)
|
||||
else
|
||||
otherRange.intersectsWith(this)
|
||||
|
||||
union: (otherRange) ->
|
||||
start = if @start.isLessThan(otherRange.start) then @start else otherRange.start
|
||||
end = if @end.isGreaterThan(otherRange.end) then @end else otherRange.end
|
||||
new Range(start, end)
|
||||
|
||||
isEmpty: ->
|
||||
@start.isEqual(@end)
|
||||
|
||||
toDelta: ->
|
||||
rows = @end.row - @start.row
|
||||
if rows == 0
|
||||
columns = @end.column - @start.column
|
||||
else
|
||||
columns = @end.column
|
||||
new Point(rows, columns)
|
||||
|
||||
222
src/app/renderer.coffee
Normal file
222
src/app/renderer.coffee
Normal file
@@ -0,0 +1,222 @@
|
||||
_ = require 'underscore'
|
||||
Highlighter = require 'highlighter'
|
||||
LineMap = require 'line-map'
|
||||
Point = require 'point'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Range = require 'range'
|
||||
Fold = require 'fold'
|
||||
ScreenLineFragment = require 'screen-line-fragment'
|
||||
foldPlaceholderLength = 3
|
||||
|
||||
module.exports =
|
||||
class Renderer
|
||||
@idCounter: 1
|
||||
lineMap: null
|
||||
highlighter: null
|
||||
activeFolds: null
|
||||
foldsById: null
|
||||
lastHighlighterChangeEvent: null
|
||||
foldPlaceholderLength: 3
|
||||
|
||||
constructor: (@buffer) ->
|
||||
@id = @constructor.idCounter++
|
||||
@highlighter = new Highlighter(@buffer)
|
||||
@maxLineLength = Infinity
|
||||
@activeFolds = {}
|
||||
@foldsById = {}
|
||||
@buildLineMap()
|
||||
@highlighter.on 'change', (e) => @lastHighlighterChangeEvent = e
|
||||
@buffer.on "change.renderer#{@id}", (e) => @handleBufferChange(e)
|
||||
|
||||
buildLineMap: ->
|
||||
@lineMap = new LineMap
|
||||
@lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
|
||||
|
||||
setMaxLineLength: (@maxLineLength) ->
|
||||
oldRange = @rangeForAllLines()
|
||||
@buildLineMap()
|
||||
newRange = @rangeForAllLines()
|
||||
@trigger 'change', { oldRange, newRange }
|
||||
|
||||
lineForRow: (row) ->
|
||||
@lineMap.lineForScreenRow(row)
|
||||
|
||||
linesForRows: (startRow, endRow) ->
|
||||
@lineMap.linesForScreenRows(startRow, endRow)
|
||||
|
||||
getLines: ->
|
||||
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
|
||||
|
||||
bufferRowsForScreenRows: ->
|
||||
@lineMap.bufferRowsForScreenRows()
|
||||
|
||||
createFold: (bufferRange) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
return if bufferRange.isEmpty()
|
||||
|
||||
fold = new Fold(this, bufferRange)
|
||||
@registerFold(bufferRange.start.row, fold)
|
||||
|
||||
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
lines = @buildLineForBufferRow(bufferRange.start.row)
|
||||
@lineMap.replaceScreenRows(
|
||||
oldScreenRange.start.row,
|
||||
oldScreenRange.end.row,
|
||||
lines)
|
||||
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
|
||||
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange
|
||||
@trigger 'fold', bufferRange
|
||||
fold
|
||||
|
||||
destroyFold: (fold) ->
|
||||
bufferRange = fold.getRange()
|
||||
@unregisterFold(bufferRange.start.row, fold)
|
||||
startScreenRow = @screenRowForBufferRow(bufferRange.start.row)
|
||||
|
||||
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
lines = @buildLinesForBufferRows(bufferRange.start.row, bufferRange.end.row)
|
||||
@lineMap.replaceScreenRows(
|
||||
oldScreenRange.start.row,
|
||||
oldScreenRange.end.row
|
||||
lines)
|
||||
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
|
||||
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange
|
||||
@trigger 'unfold', fold.getRange()
|
||||
|
||||
screenRowForBufferRow: (bufferRow) ->
|
||||
@lineMap.screenPositionForBufferPosition([bufferRow, 0]).row
|
||||
|
||||
bufferRowForScreenRow: (screenRow) ->
|
||||
@lineMap.bufferPositionForScreenPosition([screenRow, 0]).row
|
||||
|
||||
screenRangeForBufferRange: (bufferRange) ->
|
||||
@lineMap.screenRangeForBufferRange(bufferRange)
|
||||
|
||||
bufferRangeForScreenRange: (screenRange) ->
|
||||
@lineMap.bufferRangeForScreenRange(screenRange)
|
||||
|
||||
lineCount: ->
|
||||
@lineMap.screenLineCount()
|
||||
|
||||
screenPositionForBufferPosition: (position) ->
|
||||
@lineMap.screenPositionForBufferPosition(position)
|
||||
|
||||
bufferPositionForScreenPosition: (position) ->
|
||||
@lineMap.bufferPositionForScreenPosition(position)
|
||||
|
||||
clipScreenPosition: (position, options={}) ->
|
||||
@lineMap.clipScreenPosition(position, options)
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
for row, folds of @activeFolds
|
||||
for fold in new Array(folds...)
|
||||
changeInsideFold = true if fold.handleBufferChange(e)
|
||||
|
||||
unless changeInsideFold
|
||||
@handleHighlighterChange(@lastHighlighterChangeEvent)
|
||||
|
||||
handleHighlighterChange: (e) ->
|
||||
oldBufferRange = e.oldRange
|
||||
newBufferRange = e.newRange
|
||||
|
||||
oldScreenRange = @screenLineRangeForBufferRange(oldBufferRange)
|
||||
newScreenLines = @buildLinesForBufferRows(newBufferRange.start.row, newBufferRange.end.row)
|
||||
@lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines
|
||||
newScreenRange = @screenLineRangeForBufferRange(newBufferRange)
|
||||
|
||||
@trigger 'change', { oldRange: oldScreenRange, newRange: newScreenRange, bufferChanged: true }
|
||||
|
||||
buildLineForBufferRow: (bufferRow) ->
|
||||
@buildLinesForBufferRows(bufferRow, bufferRow)
|
||||
|
||||
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
|
||||
recursiveBuildLinesForBufferRows = (startBufferRow, endBufferRow, startBufferColumn, currentScreenLineLength=0) =>
|
||||
return [] if startBufferRow > endBufferRow and not startBufferColumn?
|
||||
|
||||
startBufferColumn ?= 0
|
||||
line = @highlighter.lineForRow(startBufferRow).splitAt(startBufferColumn)[1]
|
||||
|
||||
wrapScreenColumn = @findWrapColumn(line.text, @maxLineLength - currentScreenLineLength)
|
||||
|
||||
for fold in @foldsForBufferRow(startBufferRow)
|
||||
if fold.start.column >= startBufferColumn
|
||||
foldStartSceenColumn = fold.start.column - startBufferColumn
|
||||
if (foldStartSceenColumn) > wrapScreenColumn - foldPlaceholderLength
|
||||
wrapScreenColumn = Math.min(wrapScreenColumn, foldStartSceenColumn)
|
||||
break
|
||||
prefix = line.splitAt(foldStartSceenColumn)[0]
|
||||
placeholder = @buildFoldPlaceholder(fold)
|
||||
currentScreenLineLength = currentScreenLineLength + (prefix?.text.length ? 0) + foldPlaceholderLength
|
||||
suffix = recursiveBuildLinesForBufferRows(fold.end.row, endBufferRow, fold.end.column, currentScreenLineLength)
|
||||
return _.compact _.flatten [prefix, placeholder, suffix]
|
||||
|
||||
if wrapScreenColumn?
|
||||
line = line.splitAt(wrapScreenColumn)[0]
|
||||
line.screenDelta = new Point(1, 0)
|
||||
[line].concat recursiveBuildLinesForBufferRows(startBufferRow, endBufferRow, startBufferColumn + wrapScreenColumn)
|
||||
else
|
||||
[line].concat recursiveBuildLinesForBufferRows(startBufferRow + 1, endBufferRow)
|
||||
|
||||
recursiveBuildLinesForBufferRows(@foldStartRowForBufferRow(startBufferRow), endBufferRow)
|
||||
|
||||
foldStartRowForBufferRow: (bufferRow) ->
|
||||
@bufferRowForScreenRow(@screenRowForBufferRow(bufferRow))
|
||||
|
||||
findWrapColumn: (line, maxLineLength) ->
|
||||
return unless line.length > maxLineLength
|
||||
|
||||
if /\s/.test(line[maxLineLength])
|
||||
# search forward for the start of a word past the boundary
|
||||
for column in [maxLineLength..line.length]
|
||||
return column if /\S/.test(line[column])
|
||||
return line.length
|
||||
else
|
||||
# search backward for the start of the word on the boundary
|
||||
for column in [maxLineLength..0]
|
||||
return column + 1 if /\s/.test(line[column])
|
||||
return maxLineLength
|
||||
|
||||
registerFold: (bufferRow, fold) ->
|
||||
@activeFolds[bufferRow] ?= []
|
||||
@activeFolds[bufferRow].push(fold)
|
||||
@foldsById[fold.id] = fold
|
||||
|
||||
unregisterFold: (bufferRow, fold) ->
|
||||
folds = @activeFolds[bufferRow]
|
||||
folds.splice(folds.indexOf(fold), 1)
|
||||
delete @foldsById[fold.id]
|
||||
|
||||
foldsForBufferRow: (bufferRow) ->
|
||||
folds = @activeFolds[bufferRow] or []
|
||||
folds.sort (a, b) -> a.compare(b)
|
||||
|
||||
buildFoldPlaceholder: (fold) ->
|
||||
token = { value: '...', type: 'fold-placeholder', fold }
|
||||
new ScreenLineFragment([token], '...', [0, 3], fold.getRange().toDelta(), isAtomic: true)
|
||||
|
||||
screenLineRangeForBufferRange: (bufferRange) ->
|
||||
@expandScreenRangeToLineEnds(
|
||||
@lineMap.screenRangeForBufferRange(
|
||||
@expandBufferRangeToLineEnds(bufferRange)))
|
||||
|
||||
expandScreenRangeToLineEnds: (screenRange) ->
|
||||
{ start, end } = screenRange
|
||||
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
|
||||
|
||||
expandBufferRangeToLineEnds: (bufferRange) ->
|
||||
{ start, end } = bufferRange
|
||||
new Range([start.row, 0], [end.row, Infinity])
|
||||
|
||||
rangeForAllLines: ->
|
||||
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
|
||||
|
||||
destroy: ->
|
||||
@highlighter.destroy()
|
||||
@buffer.off ".renderer#{@id}"
|
||||
|
||||
logLines: ->
|
||||
@lineMap.logLines()
|
||||
|
||||
_.extend Renderer.prototype, EventEmitter
|
||||
143
src/app/root-view.coffee
Normal file
143
src/app/root-view.coffee
Normal file
@@ -0,0 +1,143 @@
|
||||
$ = require 'jquery'
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
|
||||
{View} = require 'space-pen'
|
||||
Buffer = require 'buffer'
|
||||
Editor = require 'editor'
|
||||
FileFinder = require 'file-finder'
|
||||
Project = require 'project'
|
||||
VimMode = require 'vim-mode'
|
||||
CommandPanel = require 'command-panel'
|
||||
|
||||
module.exports =
|
||||
class RootView extends View
|
||||
@content: ->
|
||||
@div id: 'root-view', tabindex: -1, =>
|
||||
@div id: 'panes', outlet: 'panes'
|
||||
|
||||
editors: null
|
||||
|
||||
initialize: ({path}) ->
|
||||
@editors = []
|
||||
@createProject(path)
|
||||
|
||||
@on 'toggle-file-finder', => @toggleFileFinder()
|
||||
@on 'show-console', -> window.showConsole()
|
||||
@on 'find-in-file', =>
|
||||
@commandPanel.show()
|
||||
@commandPanel.editor.setText("/")
|
||||
|
||||
@one 'attach', => @focus()
|
||||
@on 'focus', (e) =>
|
||||
if @editors.length
|
||||
@activeEditor().focus()
|
||||
false
|
||||
|
||||
@commandPanel = new CommandPanel({rootView: this})
|
||||
|
||||
createProject: (path) ->
|
||||
if path
|
||||
@project = new Project(fs.directory(path))
|
||||
@open(path) if fs.isFile(path)
|
||||
else
|
||||
@activeEditor().setBuffer(new Buffer())
|
||||
|
||||
open: (path) ->
|
||||
@activeEditor().setBuffer(@project.open(path))
|
||||
|
||||
editorFocused: (editor) ->
|
||||
if @panes.containsElement(editor)
|
||||
_.remove(@editors, editor)
|
||||
@editors.push(editor)
|
||||
|
||||
@setTitleToActiveEditorPath()
|
||||
|
||||
editor.on 'buffer-path-change.root-view', (event) =>
|
||||
e = $(event.target).view()
|
||||
@setTitleToActiveEditorPath()
|
||||
|
||||
editorRemoved: (editor) ->
|
||||
if @panes.containsElement
|
||||
_.remove(@editors, editor)
|
||||
@adjustSplitPanes()
|
||||
if @editors.length
|
||||
@activeEditor().focus()
|
||||
else
|
||||
window.close()
|
||||
|
||||
setTitleToActiveEditorPath: ->
|
||||
document.title = @activeEditor().buffer.path
|
||||
|
||||
activeEditor: ->
|
||||
if @editors.length
|
||||
_.last(@editors)
|
||||
else
|
||||
editor = new Editor()
|
||||
@editors.push(editor)
|
||||
editor.appendTo(@panes)
|
||||
editor.focus()
|
||||
|
||||
adjustSplitPanes: (element = @panes.children(':first'))->
|
||||
if element.hasClass('row')
|
||||
totalUnits = @horizontalGridUnits(element)
|
||||
unitsSoFar = 0
|
||||
for child in element.children()
|
||||
child = $(child)
|
||||
childUnits = @horizontalGridUnits(child)
|
||||
child.css
|
||||
width: "#{childUnits / totalUnits * 100}%"
|
||||
height: '100%'
|
||||
top: 0
|
||||
left: "#{unitsSoFar / totalUnits * 100}%"
|
||||
@adjustSplitPanes(child)
|
||||
unitsSoFar += childUnits
|
||||
|
||||
else if element.hasClass('column')
|
||||
totalUnits = @verticalGridUnits(element)
|
||||
unitsSoFar = 0
|
||||
for child in element.children()
|
||||
child = $(child)
|
||||
childUnits = @verticalGridUnits(child)
|
||||
child.css
|
||||
width: '100%'
|
||||
height: "#{childUnits / totalUnits * 100}%"
|
||||
top: "#{unitsSoFar / totalUnits * 100}%"
|
||||
left: 0
|
||||
@adjustSplitPanes(child)
|
||||
unitsSoFar += childUnits
|
||||
|
||||
horizontalGridUnits: (element) ->
|
||||
if element.is('.row, .column')
|
||||
childUnits = (@horizontalGridUnits($(child)) for child in element.children())
|
||||
if element.hasClass('row')
|
||||
_.sum(childUnits)
|
||||
else # it's a column
|
||||
Math.max(childUnits...)
|
||||
else
|
||||
1
|
||||
|
||||
verticalGridUnits: (element) ->
|
||||
if element.is('.row, .column')
|
||||
childUnits = (@verticalGridUnits($(child)) for child in element.children())
|
||||
if element.hasClass('column')
|
||||
_.sum(childUnits)
|
||||
else # it's a row
|
||||
Math.max(childUnits...)
|
||||
else
|
||||
1
|
||||
|
||||
toggleFileFinder: ->
|
||||
return unless @project
|
||||
|
||||
if @fileFinder and @fileFinder.parent()[0]
|
||||
@fileFinder.remove()
|
||||
@fileFinder = null
|
||||
@activeEditor().focus()
|
||||
else
|
||||
@project.getFilePaths().done (paths) =>
|
||||
relativePaths = (path.replace(@project.path, "") for path in paths)
|
||||
@fileFinder = new FileFinder
|
||||
paths: relativePaths
|
||||
selected: (relativePath) => @open(relativePath)
|
||||
@append @fileFinder
|
||||
54
src/app/screen-line-fragment.coffee
Normal file
54
src/app/screen-line-fragment.coffee
Normal file
@@ -0,0 +1,54 @@
|
||||
_ = require 'underscore'
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class ScreenLineFragment
|
||||
isAtomic: false
|
||||
state: null
|
||||
|
||||
constructor: (@tokens, @text, screenDelta, bufferDelta, extraFields) ->
|
||||
@screenDelta = Point.fromObject(screenDelta)
|
||||
@bufferDelta = Point.fromObject(bufferDelta)
|
||||
_.extend(this, extraFields)
|
||||
|
||||
splitAt: (column) ->
|
||||
return [new ScreenLineFragment([], '', [0, 0], [0, 0]), this] if column == 0
|
||||
|
||||
rightTokens = new Array(@tokens...)
|
||||
leftTokens = []
|
||||
leftTextLength = 0
|
||||
while leftTextLength < column
|
||||
if leftTextLength + rightTokens[0].value.length > column
|
||||
rightTokens[0..0] = @splitTokenAt(rightTokens[0], column - leftTextLength)
|
||||
nextToken = rightTokens.shift()
|
||||
leftTextLength += nextToken.value.length
|
||||
leftTokens.push nextToken
|
||||
|
||||
leftText = _.pluck(leftTokens, 'value').join('')
|
||||
rightText = _.pluck(rightTokens, 'value').join('')
|
||||
|
||||
[leftScreenDelta, rightScreenDelta] = @screenDelta.splitAt(column)
|
||||
[leftBufferDelta, rightBufferDelta] = @bufferDelta.splitAt(column)
|
||||
|
||||
leftFragment = new ScreenLineFragment(leftTokens, leftText, leftScreenDelta, leftBufferDelta, {@state})
|
||||
rightFragment = new ScreenLineFragment(rightTokens, rightText, rightScreenDelta, rightBufferDelta, {@state})
|
||||
[leftFragment, rightFragment]
|
||||
|
||||
splitTokenAt: (token, splitIndex) ->
|
||||
{ type, value } = token
|
||||
value1 = value.substring(0, splitIndex)
|
||||
value2 = value.substring(splitIndex)
|
||||
[{value: value1, type }, {value: value2, type}]
|
||||
|
||||
concat: (other) ->
|
||||
tokens = @tokens.concat(other.tokens)
|
||||
text = @text + other.text
|
||||
screenDelta = @screenDelta.add(other.screenDelta)
|
||||
bufferDelta = @bufferDelta.add(other.bufferDelta)
|
||||
new ScreenLineFragment(tokens, text, screenDelta, bufferDelta, {state: other.state})
|
||||
|
||||
isSoftWrapped: ->
|
||||
@screenDelta.row == 1 and @bufferDelta.row == 0
|
||||
|
||||
isEqual: (other) ->
|
||||
_.isEqual(@tokens, other.tokens) and @screenDelta.isEqual(other.screenDelta) and @bufferDelta.isEqual(other.bufferDelta)
|
||||
228
src/app/selection.coffee
Normal file
228
src/app/selection.coffee
Normal file
@@ -0,0 +1,228 @@
|
||||
Anchor = require 'anchor'
|
||||
Cursor = require 'cursor'
|
||||
AceOutdentAdaptor = require 'ace-outdent-adaptor'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
{View, $$} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class Selection extends View
|
||||
@content: ->
|
||||
@div()
|
||||
|
||||
anchor: null
|
||||
retainSelection: null
|
||||
regions: null
|
||||
|
||||
initialize: ({@editor, @cursor}) ->
|
||||
@regions = []
|
||||
@cursor.on 'cursor:position-changed', => @updateAppearance()
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
return unless @anchor
|
||||
@anchor.handleBufferChange(e)
|
||||
|
||||
placeAnchor: ->
|
||||
return if @anchor
|
||||
@anchor = new Anchor(@editor)
|
||||
@anchor.setScreenPosition @cursor.getScreenPosition()
|
||||
|
||||
isEmpty: ->
|
||||
@getBufferRange().isEmpty()
|
||||
|
||||
isReversed: ->
|
||||
not @isEmpty() and @cursor.getBufferPosition().isLessThan(@anchor.getBufferPosition())
|
||||
|
||||
intersectsWith: (otherSelection) ->
|
||||
@getScreenRange().intersectsWith(otherSelection.getScreenRange())
|
||||
|
||||
clearSelection: ->
|
||||
@anchor = null
|
||||
@updateAppearance()
|
||||
|
||||
updateAppearance: ->
|
||||
return unless @cursor
|
||||
|
||||
@clearRegions()
|
||||
|
||||
range = @getScreenRange()
|
||||
return if range.isEmpty()
|
||||
|
||||
rowSpan = range.end.row - range.start.row
|
||||
|
||||
if rowSpan == 0
|
||||
@appendRegion(1, range.start, range.end)
|
||||
else
|
||||
@appendRegion(1, range.start, null)
|
||||
if rowSpan > 1
|
||||
@appendRegion(rowSpan - 1, { row: range.start.row + 1, column: 0}, null)
|
||||
@appendRegion(1, { row: range.end.row, column: 0 }, range.end)
|
||||
|
||||
appendRegion: (rows, start, end) ->
|
||||
{ lineHeight, charWidth } = @editor
|
||||
css = @editor.pixelPositionForScreenPosition(start)
|
||||
css.height = lineHeight * rows
|
||||
if end
|
||||
css.width = @editor.pixelPositionForScreenPosition(end).left - css.left
|
||||
else
|
||||
css.right = 0
|
||||
|
||||
region = ($$ -> @div class: 'selection').css(css)
|
||||
@append(region)
|
||||
@regions.push(region)
|
||||
|
||||
clearRegions: ->
|
||||
region.remove() for region in @regions
|
||||
@regions = []
|
||||
|
||||
getScreenRange: ->
|
||||
if @anchor
|
||||
new Range(@anchor.getScreenPosition(), @cursor.getScreenPosition())
|
||||
else
|
||||
new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition())
|
||||
|
||||
setScreenRange: (range, options={}) ->
|
||||
{ reverse } = options
|
||||
{ start, end } = range
|
||||
[start, end] = [end, start] if reverse
|
||||
|
||||
@cursor.setScreenPosition(start)
|
||||
@modifySelection => @cursor.setScreenPosition(end)
|
||||
|
||||
getBufferRange: ->
|
||||
@editor.bufferRangeForScreenRange(@getScreenRange())
|
||||
|
||||
setBufferRange: (bufferRange, options) ->
|
||||
@setScreenRange(@editor.screenRangeForBufferRange(bufferRange), options)
|
||||
|
||||
getText: ->
|
||||
@editor.buffer.getTextInRange @getBufferRange()
|
||||
|
||||
insertText: (text) ->
|
||||
{ text, shouldOutdent } = @autoIndentText(text)
|
||||
@editor.buffer.change(@getBufferRange(), text)
|
||||
@autoOutdentText() if shouldOutdent
|
||||
@clearSelection()
|
||||
|
||||
autoIndentText: (text) ->
|
||||
if @editor.autoIndent
|
||||
mode = @editor.getCurrentMode()
|
||||
row = @cursor.getScreenPosition().row
|
||||
state = @editor.stateForScreenRow(row)
|
||||
if text[0] == "\n"
|
||||
indent = mode.getNextLineIndent(state, @cursor.getCurrentBufferLine(), atom.tabText)
|
||||
text = text[0] + indent + text[1..]
|
||||
else if mode.checkOutdent(state, @cursor.getCurrentBufferLine(), text)
|
||||
shouldOutdent = true
|
||||
|
||||
{text, shouldOutdent}
|
||||
|
||||
autoOutdentText: ->
|
||||
screenRow = @cursor.getScreenPosition().row
|
||||
bufferRow = @cursor.getBufferPosition().row
|
||||
state = @editor.renderer.lineForRow(screenRow).state
|
||||
@editor.getCurrentMode().autoOutdent(state, new AceOutdentAdaptor(@editor.buffer, @editor), bufferRow)
|
||||
|
||||
backspace: ->
|
||||
@selectLeft() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
backspaceToBeginningOfWord: ->
|
||||
@selectToBeginningOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
delete: ->
|
||||
@selectRight() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
deleteToEndOfWord: ->
|
||||
@selectToEndOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
deleteSelectedText: ->
|
||||
range = @getBufferRange()
|
||||
@editor.buffer.delete(range) unless range.isEmpty()
|
||||
@clearSelection()
|
||||
|
||||
merge: (otherSelection, options) ->
|
||||
@setScreenRange(@getScreenRange().union(otherSelection.getScreenRange()), options)
|
||||
otherSelection.remove()
|
||||
|
||||
remove: ->
|
||||
@cursor?.remove()
|
||||
super
|
||||
|
||||
modifySelection: (fn) ->
|
||||
@placeAnchor()
|
||||
@retainSelection = true
|
||||
fn()
|
||||
@retainSelection = false
|
||||
|
||||
selectWord: ->
|
||||
row = @cursor.getScreenPosition().row
|
||||
column = @cursor.getScreenPosition().column
|
||||
|
||||
{ row, column } = @cursor.getBufferPosition()
|
||||
|
||||
line = @editor.buffer.lineForRow(row)
|
||||
leftSide = line[0...column].split('').reverse().join('') # reverse left side
|
||||
rightSide = line[column..]
|
||||
|
||||
regex = /^\w*/
|
||||
startOffset = -regex.exec(leftSide)?[0]?.length or 0
|
||||
endOffset = regex.exec(rightSide)?[0]?.length or 0
|
||||
|
||||
range = new Range([row, column + startOffset], [row, column + endOffset])
|
||||
@setBufferRange range
|
||||
|
||||
selectLine: (row=@cursor.getBufferPosition().row) ->
|
||||
rowLength = @editor.buffer.lineForRow(row).length
|
||||
@setBufferRange new Range([row, 0], [row, rowLength])
|
||||
|
||||
selectToScreenPosition: (position) ->
|
||||
@modifySelection => @cursor.setScreenPosition(position)
|
||||
|
||||
selectRight: ->
|
||||
@modifySelection => @cursor.moveRight()
|
||||
|
||||
selectLeft: ->
|
||||
@modifySelection => @cursor.moveLeft()
|
||||
|
||||
selectUp: ->
|
||||
@modifySelection => @cursor.moveUp()
|
||||
|
||||
selectDown: ->
|
||||
@modifySelection => @cursor.moveDown()
|
||||
|
||||
selectToTop: ->
|
||||
@modifySelection => @cursor.moveToTop()
|
||||
|
||||
selectToBottom: ->
|
||||
@modifySelection => @cursor.moveToBottom()
|
||||
|
||||
selectToBeginningOfLine: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfLine()
|
||||
|
||||
selectToEndOfLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfLine()
|
||||
|
||||
selectToBeginningOfWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfWord()
|
||||
|
||||
selectToEndOfWord: ->
|
||||
@modifySelection => @cursor.moveToEndOfWord()
|
||||
|
||||
cut: (maintainPasteboard=false) ->
|
||||
@copy(maintainPasteboard)
|
||||
@delete()
|
||||
|
||||
copy: (maintainPasteboard=false) ->
|
||||
return if @isEmpty()
|
||||
text = @editor.buffer.getTextInRange(@getBufferRange())
|
||||
text = $native.readFromPasteboard() + "\n" + text if maintainPasteboard
|
||||
$native.writeToPasteboard text
|
||||
|
||||
fold: ->
|
||||
range = @getBufferRange()
|
||||
@editor.createFold(range)
|
||||
@cursor.setBufferPosition(range.end)
|
||||
30
src/app/undo-manager.coffee
Normal file
30
src/app/undo-manager.coffee
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports =
|
||||
class UndoManager
|
||||
undoHistory: null
|
||||
redoHistory: null
|
||||
preserveHistory: false
|
||||
|
||||
constructor: (@buffer) ->
|
||||
@undoHistory = []
|
||||
@redoHistory = []
|
||||
@buffer.on 'change', (op) =>
|
||||
unless @preserveHistory
|
||||
@undoHistory.push(op)
|
||||
@redoHistory = []
|
||||
|
||||
undo: ->
|
||||
if op = @undoHistory.pop()
|
||||
@preservingHistory =>
|
||||
@buffer.change op.newRange, op.oldText
|
||||
@redoHistory.push op
|
||||
|
||||
redo: ->
|
||||
if op = @redoHistory.pop()
|
||||
@preservingHistory =>
|
||||
@buffer.change op.oldRange, op.newText
|
||||
@undoHistory.push op
|
||||
|
||||
preservingHistory: (fn) ->
|
||||
@preserveHistory = true
|
||||
fn()
|
||||
@preserveHistory = false
|
||||
130
src/app/vim-mode.coffee
Normal file
130
src/app/vim-mode.coffee
Normal file
@@ -0,0 +1,130 @@
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
operators = require 'vim-mode/operators'
|
||||
commands = require 'vim-mode/commands'
|
||||
motions = require 'vim-mode/motions'
|
||||
|
||||
module.exports =
|
||||
class VimMode
|
||||
editor: null
|
||||
opStack: null
|
||||
|
||||
constructor: (@editor) ->
|
||||
requireStylesheet 'vim-mode.css'
|
||||
|
||||
@opStack = []
|
||||
@activateCommandMode()
|
||||
|
||||
window.keymap.bindKeys '.editor', 'escape': 'activate-command-mode'
|
||||
@editor.on 'activate-command-mode', => @activateCommandMode()
|
||||
|
||||
@setupCommandMode()
|
||||
|
||||
setupCommandMode: ->
|
||||
window.keymap.bindKeys '.command-mode', (e) =>
|
||||
if e.keystroke.match /^\d$/
|
||||
return 'command-mode:numeric-prefix'
|
||||
if e.keystroke.match /^.$/
|
||||
@resetCommandMode()
|
||||
return false
|
||||
|
||||
@bindCommandModeKeys
|
||||
'i': 'insert'
|
||||
'd': 'delete'
|
||||
'x': 'delete-right'
|
||||
'h': 'move-left'
|
||||
'j': 'move-down'
|
||||
'k': 'move-up'
|
||||
'l': 'move-right'
|
||||
'w': 'move-to-next-word'
|
||||
'b': 'move-to-previous-word'
|
||||
'}': 'move-to-next-paragraph'
|
||||
'escape': 'reset-command-mode'
|
||||
'left': 'move-left'
|
||||
'right': 'move-right'
|
||||
|
||||
@handleCommands
|
||||
'insert': => @activateInsertMode()
|
||||
'delete': => @delete()
|
||||
'delete-right': => new commands.DeleteRight(@editor)
|
||||
'move-left': => new motions.MoveLeft(@editor)
|
||||
'move-up': => new motions.MoveUp(@editor)
|
||||
'move-down': => new motions.MoveDown @editor
|
||||
'move-right': => new motions.MoveRight @editor
|
||||
'move-to-next-word': => new motions.MoveToNextWord(@editor)
|
||||
'move-to-previous-word': => new motions.MoveToPreviousWord(@editor)
|
||||
'move-to-next-paragraph': => new motions.MoveToNextParagraph(@editor)
|
||||
'numeric-prefix': (e) => @numericPrefix(e)
|
||||
'reset-command-mode': => @resetCommandMode()
|
||||
|
||||
bindCommandModeKeys: (bindings) ->
|
||||
prefixedBindings = {}
|
||||
for pattern, commandName of bindings
|
||||
prefixedBindings[pattern] = "command-mode:#{commandName}"
|
||||
|
||||
window.keymap.bindKeys ".command-mode", prefixedBindings
|
||||
|
||||
handleCommands: (commands) ->
|
||||
_.each commands, (fn, commandName) =>
|
||||
eventName = "command-mode:#{commandName}"
|
||||
@editor.on eventName, (e) =>
|
||||
possibleOperator = fn(e)
|
||||
@pushOperator(possibleOperator) if possibleOperator?.execute
|
||||
|
||||
activateInsertMode: ->
|
||||
@editor.removeClass('command-mode')
|
||||
@editor.addClass('insert-mode')
|
||||
|
||||
@editor.off 'cursor:position-changed', @moveCursorBeforeNewline
|
||||
|
||||
activateCommandMode: ->
|
||||
@editor.removeClass('insert-mode')
|
||||
@editor.addClass('command-mode')
|
||||
|
||||
@editor.on 'cursor:position-changed', @moveCursorBeforeNewline
|
||||
|
||||
resetCommandMode: ->
|
||||
@opStack = []
|
||||
|
||||
moveCursorBeforeNewline: =>
|
||||
if not @editor.getSelection().modifyingSelection and @editor.cursor.isOnEOL() and @editor.getCurrentBufferLine().length > 0
|
||||
@editor.setCursorBufferColumn(@editor.getCurrentBufferLine().length - 1)
|
||||
|
||||
numericPrefix: (e) ->
|
||||
num = parseInt(e.keyEvent.keystroke)
|
||||
if @topOperator() instanceof operators.NumericPrefix
|
||||
@topOperator().addDigit(num)
|
||||
else
|
||||
@pushOperator(new operators.NumericPrefix(num))
|
||||
|
||||
delete: () ->
|
||||
if deleteOperation = @isDeletePending()
|
||||
deleteOperation.complete = true
|
||||
@processOpStack()
|
||||
else
|
||||
@pushOperator(new operators.Delete(@editor))
|
||||
|
||||
isDeletePending: () ->
|
||||
for op in @opStack
|
||||
return op if op instanceof operators.Delete
|
||||
false
|
||||
|
||||
pushOperator: (op) ->
|
||||
@opStack.push(op)
|
||||
@processOpStack()
|
||||
|
||||
processOpStack: ->
|
||||
return unless @topOperator().isComplete()
|
||||
|
||||
poppedOperator = @opStack.pop()
|
||||
if @opStack.length
|
||||
try
|
||||
@topOperator().compose(poppedOperator)
|
||||
@processOpStack()
|
||||
catch e
|
||||
(e instanceof operators.OperatorError) and @resetCommandMode() or throw e
|
||||
else
|
||||
poppedOperator.execute()
|
||||
|
||||
topOperator: ->
|
||||
_.last @opStack
|
||||
10
src/app/vim-mode/commands.coffee
Normal file
10
src/app/vim-mode/commands.coffee
Normal file
@@ -0,0 +1,10 @@
|
||||
class Command
|
||||
constructor: (@editor) ->
|
||||
isComplete: -> true
|
||||
|
||||
class DeleteRight extends Command
|
||||
execute: ->
|
||||
@editor.delete() unless @editor.getCurrentBufferLine().length == 0
|
||||
|
||||
module.exports = { DeleteRight }
|
||||
|
||||
89
src/app/vim-mode/motions.coffee
Normal file
89
src/app/vim-mode/motions.coffee
Normal file
@@ -0,0 +1,89 @@
|
||||
Point = require 'point'
|
||||
getWordRegex = -> /(\w+)|([^\w\s]+)/g
|
||||
|
||||
class Motion
|
||||
constructor: (@editor) ->
|
||||
isComplete: -> true
|
||||
|
||||
class MoveLeft extends Motion
|
||||
execute: ->
|
||||
{column, row} = @editor.getCursorScreenPosition()
|
||||
@editor.moveCursorLeft() if column > 0
|
||||
|
||||
select: ->
|
||||
position = @editor.getCursorScreenPosition().copy()
|
||||
position.column-- if position.column > 0
|
||||
@editor.selectToBufferPosition(position)
|
||||
|
||||
class MoveRight extends Motion
|
||||
execute: ->
|
||||
{column, row} = @editor.getCursorScreenPosition()
|
||||
@editor.moveCursorRight()
|
||||
|
||||
class MoveUp extends Motion
|
||||
execute: ->
|
||||
{column, row} = @editor.getCursorScreenPosition()
|
||||
@editor.moveCursorUp() if row > 0
|
||||
|
||||
class MoveDown extends Motion
|
||||
execute: ->
|
||||
{column, row} = @editor.getCursorScreenPosition()
|
||||
@editor.moveCursorDown() if row < (@editor.buffer.numLines() - 1)
|
||||
|
||||
class MoveToPreviousWord extends Motion
|
||||
execute: ->
|
||||
@editor.getCursor().moveToBeginningOfWord()
|
||||
|
||||
select: ->
|
||||
@editor.getSelection().selectToBeginningOfWord()
|
||||
|
||||
class MoveToNextWord extends Motion
|
||||
execute: ->
|
||||
@editor.setCursorScreenPosition(@nextWordPosition())
|
||||
|
||||
select: ->
|
||||
@editor.selectToBufferPosition(@nextWordPosition())
|
||||
|
||||
nextWordPosition: ->
|
||||
regex = getWordRegex()
|
||||
{ row, column } = @editor.getCursorScreenPosition()
|
||||
rightOfCursor = @editor.buffer.lineForRow(row).substring(column)
|
||||
|
||||
match = regex.exec(rightOfCursor)
|
||||
# If we're on top of part of a word, match the next one.
|
||||
match = regex.exec(rightOfCursor) if match?.index is 0
|
||||
|
||||
if match
|
||||
column += match.index
|
||||
else if row + 1 == @editor.buffer.numLines()
|
||||
column = @editor.buffer.lineForRow(row).length
|
||||
else
|
||||
nextLineMatch = regex.exec(@editor.buffer.lineForRow(++row))
|
||||
column = nextLineMatch?.index or 0
|
||||
{ row, column }
|
||||
|
||||
class MoveToNextParagraph extends Motion
|
||||
execute: ->
|
||||
@editor.setCursorScreenPosition(@nextPosition())
|
||||
|
||||
select: ->
|
||||
@editor.selectToPosition(@nextPosition())
|
||||
|
||||
nextPosition: ->
|
||||
regex = /[^\n]\n^$/gm
|
||||
row = null
|
||||
column = 0
|
||||
|
||||
startRow = @editor.getCursorBufferRow() + 1
|
||||
for r in [startRow..@editor.buffer.getLastRow()]
|
||||
if @editor.buffer.lineForRow(r).length == 0
|
||||
row = r
|
||||
break
|
||||
|
||||
if not row
|
||||
row = @editor.buffer.getLastRow()
|
||||
column = @editor.buffer.lastLine().length - 1
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
module.exports = { Motion, MoveLeft, MoveRight, MoveUp, MoveDown, MoveToNextWord, MoveToPreviousWord, MoveToNextParagraph }
|
||||
57
src/app/vim-mode/operators.coffee
Normal file
57
src/app/vim-mode/operators.coffee
Normal file
@@ -0,0 +1,57 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
class OperatorError
|
||||
constructor: (@message) ->
|
||||
@name = "Operator Error"
|
||||
|
||||
class NumericPrefix
|
||||
count: null
|
||||
complete: null
|
||||
operatorToRepeat: null
|
||||
|
||||
constructor: (@count) ->
|
||||
@complete = false
|
||||
|
||||
isComplete: -> @complete
|
||||
|
||||
compose: (@operatorToRepeat) ->
|
||||
@complete = true
|
||||
if @operatorToRepeat.setCount?
|
||||
@operatorToRepeat.setCount @count
|
||||
@count = 1
|
||||
|
||||
addDigit: (digit) ->
|
||||
@count = @count * 10 + digit
|
||||
|
||||
execute: ->
|
||||
_.times @count, => @operatorToRepeat.execute()
|
||||
|
||||
select: ->
|
||||
_.times @count, => @operatorToRepeat.select()
|
||||
|
||||
class Delete
|
||||
motion: null
|
||||
complete: null
|
||||
|
||||
constructor: (@editor) ->
|
||||
@complete = false
|
||||
|
||||
isComplete: -> @complete
|
||||
|
||||
execute: ->
|
||||
if @motion
|
||||
@motion.select()
|
||||
@editor.getSelection().delete()
|
||||
else
|
||||
@editor.buffer.deleteRow(@editor.getCursorBufferRow())
|
||||
@editor.setCursorScreenPosition([@editor.getCursorScreenRow(), 0])
|
||||
|
||||
compose: (motion) ->
|
||||
if not motion.select
|
||||
throw new OperatorError("Delete must compose with a motion")
|
||||
|
||||
@motion = motion
|
||||
@complete = true
|
||||
|
||||
module.exports = { NumericPrefix, Delete, OperatorError }
|
||||
|
||||
68
src/app/window.coffee
Normal file
68
src/app/window.coffee
Normal file
@@ -0,0 +1,68 @@
|
||||
# This a weirdo file. We don't create a Window class, we just add stuff to
|
||||
# the DOM window.
|
||||
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
|
||||
windowAdditions =
|
||||
rootViewParentSelector: 'body'
|
||||
rootView: null
|
||||
keymap: null
|
||||
|
||||
setUpKeymap: ->
|
||||
Keymap = require 'keymap'
|
||||
|
||||
@keymap = new Keymap()
|
||||
@keymap.bindDefaultKeys()
|
||||
require(keymapPath) for keymapPath in fs.list(require.resolve("keymaps"))
|
||||
|
||||
@_handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
|
||||
$(document).on 'keydown', @_handleKeyEvent
|
||||
|
||||
startup: (path) ->
|
||||
@attachRootView(path)
|
||||
@loadUserConfiguration()
|
||||
$(window).on 'close', => @close()
|
||||
$(window).focus()
|
||||
atom.windowOpened this
|
||||
|
||||
shutdown: ->
|
||||
@rootView.remove()
|
||||
$(window).unbind('focus')
|
||||
$(window).unbind('blur')
|
||||
atom.windowClosed this
|
||||
|
||||
attachRootView: (path) ->
|
||||
@rootView = new RootView {path}
|
||||
$(@rootViewParentSelector).append @rootView
|
||||
|
||||
loadUserConfiguration: ->
|
||||
try
|
||||
require atom.userConfigurationPath if fs.exists(atom.userConfigurationPath)
|
||||
catch error
|
||||
console.error "Failed to load `#{atom.userConfigurationPath}`", error
|
||||
@showConsole()
|
||||
|
||||
requireStylesheet: (path) ->
|
||||
fullPath = require.resolve(path)
|
||||
content = fs.read(fullPath)
|
||||
return if $("head style[path='#{fullPath}']").length
|
||||
$('head').append "<style path='#{fullPath}'>#{content}</style>"
|
||||
|
||||
showConsole: ->
|
||||
$native.showDevTools()
|
||||
|
||||
onerror: ->
|
||||
@showConsole()
|
||||
|
||||
window[key] = value for key, value of windowAdditions
|
||||
window.setUpKeymap()
|
||||
|
||||
RootView = require 'root-view'
|
||||
|
||||
require 'jquery-extensions'
|
||||
require 'underscore-extensions'
|
||||
|
||||
requireStylesheet 'reset.css'
|
||||
requireStylesheet 'atom.css'
|
||||
Reference in New Issue
Block a user