mirror of
https://github.com/atom/atom.git
synced 2026-01-15 01:48:15 -05:00
WIP: Use rake to start compiling resources (like require.coffee)
This commit is contained in:
120
Rakefile
Normal file
120
Rakefile
Normal file
@@ -0,0 +1,120 @@
|
||||
require 'fileutils'
|
||||
|
||||
$ATOM_ARGS = []
|
||||
|
||||
ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin/"
|
||||
BUILD_DIR = 'atom-build'
|
||||
mkdir_p BUILD_DIR
|
||||
|
||||
desc "Build Atom via `xcodebuild`"
|
||||
task :build => :"verify-prerequisites" do
|
||||
output = `xcodebuild -scheme atom-release SYMROOT=#{BUILD_DIR}`
|
||||
if $?.exitstatus != 0
|
||||
$stderr.puts "Error #{$?.exitstatus}:\n#{output}"
|
||||
exit($?.exitstatus)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Clean build Atom via `xcodebuild`"
|
||||
task :clean do
|
||||
output = `xcodebuild clean SYMROOT=#{BUILD_DIR}`
|
||||
rm_rf BUILD_DIR
|
||||
end
|
||||
|
||||
desc "Create the Atom.app for distribution"
|
||||
task :package => :build do
|
||||
if path = application_path()
|
||||
FileUtils.rm_rf "pkg"
|
||||
FileUtils.mkdir_p "pkg"
|
||||
FileUtils.cp_r path, "pkg/"
|
||||
`cd pkg && zip -r atom.zip .`
|
||||
else
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Run Atom"
|
||||
task :run => :build do
|
||||
if path = binary_path()
|
||||
exitstatus = system "#{path} #{$ATOM_ARGS.join(' ')} 2> /dev/null"
|
||||
exit(exitstatus)
|
||||
else
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Run the specs"
|
||||
task :test => :clean do
|
||||
$ATOM_ARGS.push "--test", "--headless"
|
||||
Rake::Task["run"].invoke
|
||||
end
|
||||
|
||||
desc "Run the benchmarks"
|
||||
task :benchmark do
|
||||
$ATOM_ARGS.push "--benchmark", "--headless"
|
||||
Rake::Task["run"].invoke
|
||||
end
|
||||
|
||||
desc "Copy files to bundle and compile CoffeeScripts"
|
||||
task :"copy-files-to-bundle" => :"verify-prerequisites" do
|
||||
project_dir = ENV['PROJECT_DIR'] || '.'
|
||||
built_dir = ENV['BUILT_PRODUCTS_DIR'] || '.'
|
||||
contents_dir = ENV['CONTENTS_FOLDER_PATH'].to_s
|
||||
|
||||
dest = File.join(built_dir, contents_dir, "Resources")
|
||||
|
||||
rm_rf File.join(dest, 'static')
|
||||
cp_r 'static', File.join(dest, 'static')
|
||||
|
||||
sh "coffee -c -o #{dest}/src/stdlib src/stdlib/require.coffee"
|
||||
cp "src/stdlib/onig-reg-exp-extension.js", "#{dest}/src/stdlib"
|
||||
# unless ENV['LOAD_RESOURCES_FROM_DIR']
|
||||
# %w(src static vendor spec benchmark bundles themes).each do |dir|
|
||||
# rm_rf File.join(dest, dir)
|
||||
# cp_r dir, File.join(dest, dir)
|
||||
# end
|
||||
|
||||
# sh "coffee -c #{dest}/src #{dest}/vendor #{dest}/spec #{dest}/benchmark"
|
||||
# end
|
||||
end
|
||||
|
||||
desc "Remove any 'fit' or 'fdescribe' focus directives from the specs"
|
||||
task :nof do
|
||||
system %{find . -name *spec.coffee | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"}
|
||||
end
|
||||
|
||||
task :"verify-prerequisites" do
|
||||
`hash coffee`
|
||||
if not $?.success?
|
||||
abort "error: coffee is required but it's not installed - " +
|
||||
"http://coffeescript.org/ - (try `npm i -g coffee-script`)"
|
||||
end
|
||||
end
|
||||
|
||||
def application_path
|
||||
applications = FileList["#{BUILD_DIR}/**/Atom.app"]
|
||||
if applications.size == 0
|
||||
$stderr.puts "No Atom application found in directory `#{BUILD_DIR}`"
|
||||
elsif applications.size > 1
|
||||
$stderr.puts "Multiple Atom applications found \n\t" + applications.join("\n\t")
|
||||
else
|
||||
return applications.first
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
def binary_path
|
||||
if app_path = application_path()
|
||||
binary_path = "#{app_path}/Contents/MacOS/Atom"
|
||||
if File.exists?(binary_path)
|
||||
return binary_path
|
||||
else
|
||||
$stderr.puts "Executable `#{app_path}` not found."
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
9
atom.gyp
9
atom.gyp
@@ -126,12 +126,11 @@
|
||||
],
|
||||
'postbuilds': [
|
||||
{
|
||||
'postbuild_name': 'Copy static files',
|
||||
'postbuild_name': 'Copy and Compile Static Files',
|
||||
'action': [
|
||||
'cp',
|
||||
'-r',
|
||||
'static/',
|
||||
'${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Resources/',
|
||||
'rake',
|
||||
'--trace',
|
||||
'copy-files-to-bundle',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
window_info.SetAsChild(self.webView, 0, 0, self.webView.bounds.size.width, self.webView.bounds.size.height);
|
||||
|
||||
NSURL *url = [[NSBundle mainBundle] resourceURL];
|
||||
NSString *urlString = [[url URLByAppendingPathComponent:@"index.html"] absoluteString];
|
||||
NSString *urlString = [[url URLByAppendingPathComponent:@"static/index.html"] absoluteString];
|
||||
urlString = [urlString stringByAppendingFormat:@"?bootstrapScript=%@.js", _bootstrapScript];
|
||||
|
||||
NSLog(@"%@", urlString);
|
||||
|
||||
28
src/app/ace-adaptor.coffee
Normal file
28
src/app/ace-adaptor.coffee
Normal file
@@ -0,0 +1,28 @@
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class AceAdaptor
|
||||
foldWidgets: {}
|
||||
|
||||
constructor: (@editSession) ->
|
||||
@buffer = @editSession.buffer
|
||||
|
||||
getLine: (bufferRow) ->
|
||||
@buffer.lineForRow(bufferRow)
|
||||
|
||||
getLength: ->
|
||||
@buffer.getLineCount()
|
||||
|
||||
$findClosingBracket: (bracketType, bufferPosition) ->
|
||||
@editSession.tokenizedBuffer.findClosingBracket([bufferPosition.row, bufferPosition.column - 1])
|
||||
|
||||
indentRows: (startRow, endRow, indentString) ->
|
||||
for row in [startRow..endRow]
|
||||
@buffer.insert([row, 0], indentString)
|
||||
|
||||
replace: (range, text) ->
|
||||
range = Range.fromObject(range)
|
||||
@buffer.change(range, text)
|
||||
|
||||
findMatchingBracket: ({row, column}) ->
|
||||
@editSession.tokenizedBuffer.findOpeningBracket([row, column])
|
||||
28
src/app/anchor-range.coffee
Normal file
28
src/app/anchor-range.coffee
Normal file
@@ -0,0 +1,28 @@
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class AnchorRange
|
||||
start: null
|
||||
end: null
|
||||
buffer: null
|
||||
editSession: null # optional
|
||||
|
||||
constructor: (bufferRange, @buffer, @editSession) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
@startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreEqual: true)
|
||||
@endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
|
||||
|
||||
getBufferRange: ->
|
||||
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
|
||||
|
||||
getScreenRange: ->
|
||||
new Range(@startAnchor.getScreenPosition(), @endAnchor.getScreenPosition())
|
||||
|
||||
containsBufferPosition: (bufferPosition) ->
|
||||
@getBufferRange().containsPoint(bufferPosition)
|
||||
|
||||
destroy: ->
|
||||
@startAnchor.destroy()
|
||||
@endAnchor.destroy()
|
||||
@buffer.removeAnchorRange(this)
|
||||
@editSession?.removeAnchorRange(this)
|
||||
81
src/app/anchor.coffee
Normal file
81
src/app/anchor.coffee
Normal file
@@ -0,0 +1,81 @@
|
||||
Point = require 'point'
|
||||
EventEmitter = require 'event-emitter'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Anchor
|
||||
buffer: null
|
||||
editSession: null # optional
|
||||
bufferPosition: null
|
||||
screenPosition: null
|
||||
ignoreEqual: false
|
||||
strong: false
|
||||
|
||||
constructor: (@buffer, options = {}) ->
|
||||
{ @editSession, @ignoreEqual, @strong } = options
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
{ oldRange, newRange } = e
|
||||
position = @getBufferPosition()
|
||||
|
||||
if oldRange.containsPoint(position, exclusive: true)
|
||||
if @strong
|
||||
@setBufferPosition(oldRange.start)
|
||||
else
|
||||
@destroy()
|
||||
return
|
||||
|
||||
if @ignoreEqual
|
||||
return if position.isLessThanOrEqual(oldRange.end)
|
||||
else
|
||||
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], bufferChange: true)
|
||||
|
||||
getBufferPosition: ->
|
||||
@bufferPosition
|
||||
|
||||
setBufferPosition: (position, options={}) ->
|
||||
@bufferPosition = Point.fromObject(position)
|
||||
clip = options.clip ? true
|
||||
@bufferPosition = @buffer.clipPosition(@bufferPosition) if clip
|
||||
@refreshScreenPosition(options)
|
||||
|
||||
getScreenPosition: ->
|
||||
@screenPosition
|
||||
|
||||
setScreenPosition: (position, options={}) ->
|
||||
previousScreenPosition = @screenPosition
|
||||
@screenPosition = Point.fromObject(position)
|
||||
clip = options.clip ? true
|
||||
assignBufferPosition = options.assignBufferPosition ? true
|
||||
|
||||
@screenPosition = @editSession.clipScreenPosition(@screenPosition, options) if clip
|
||||
@bufferPosition = @editSession.bufferPositionForScreenPosition(@screenPosition, options) if assignBufferPosition
|
||||
|
||||
Object.freeze @screenPosition
|
||||
Object.freeze @bufferPosition
|
||||
|
||||
unless @screenPosition.isEqual(previousScreenPosition)
|
||||
@trigger 'change-screen-position', @screenPosition, bufferChange: options.bufferChange
|
||||
|
||||
refreshScreenPosition: (options={}) ->
|
||||
return unless @editSession
|
||||
screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options)
|
||||
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false)
|
||||
|
||||
destroy: ->
|
||||
@buffer.removeAnchor(this)
|
||||
@editSession?.removeAnchor(this)
|
||||
@trigger 'destroy'
|
||||
@off()
|
||||
|
||||
_.extend(Anchor.prototype, EventEmitter)
|
||||
39
src/app/atom.coffee
Normal file
39
src/app/atom.coffee
Normal file
@@ -0,0 +1,39 @@
|
||||
Keymap = require 'keymap'
|
||||
fs = require 'fs'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
require 'underscore-extensions'
|
||||
|
||||
module.exports =
|
||||
class Atom
|
||||
keymap: null
|
||||
windows: null
|
||||
configFilePath: null
|
||||
configDirPath: null
|
||||
rootViewStates: null
|
||||
|
||||
constructor: (@loadPath, nativeMethods) ->
|
||||
@windows = []
|
||||
@setUpKeymap()
|
||||
@configDirPath = fs.absolute("~/.atom")
|
||||
@configFilePath = fs.join(@configDirPath, "atom.coffee")
|
||||
@rootViewStates = {}
|
||||
|
||||
setUpKeymap: ->
|
||||
@keymap = new Keymap()
|
||||
@handleKeyEvent = (e) => @keymap.handleKeyEvent(e)
|
||||
$(document).on 'keydown', @handleKeyEvent
|
||||
@keymap.bindDefaultKeys()
|
||||
|
||||
open: (path) ->
|
||||
$native.open path
|
||||
|
||||
quit: ->
|
||||
$native.terminate null
|
||||
|
||||
windowOpened: (window) ->
|
||||
@windows.push(window) unless _.contains(@windows, window)
|
||||
|
||||
windowClosed: (window) ->
|
||||
_.remove(@windows, window)
|
||||
52
src/app/binding-set.coffee
Normal file
52
src/app/binding-set.coffee
Normal file
@@ -0,0 +1,52 @@
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
|
||||
Specificity = require 'specificity'
|
||||
PEG = require 'pegjs'
|
||||
|
||||
module.exports =
|
||||
class BindingSet
|
||||
selector: null
|
||||
commandsByKeystrokes: null
|
||||
commandForEvent: null
|
||||
parser: null
|
||||
|
||||
constructor: (@selector, mapOrFunction, @index) ->
|
||||
@parser = PEG.buildParser(fs.read(require.resolve 'keystroke-pattern.pegjs'))
|
||||
@specificity = Specificity(@selector)
|
||||
@commandsByKeystrokes = {}
|
||||
|
||||
if _.isFunction(mapOrFunction)
|
||||
@commandForEvent = mapOrFunction
|
||||
else
|
||||
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(mapOrFunction)
|
||||
@commandForEvent = (event) =>
|
||||
for keystrokes, command of @commandsByKeystrokes
|
||||
return command if event.keystrokes == keystrokes
|
||||
null
|
||||
|
||||
matchesKeystrokePrefix: (event) ->
|
||||
eventKeystrokes = event.keystrokes.split(' ')
|
||||
for keystrokes, command of @commandsByKeystrokes
|
||||
bindingKeystrokes = keystrokes.split(' ')
|
||||
continue unless eventKeystrokes.length < bindingKeystrokes.length
|
||||
return true if _.isEqual(eventKeystrokes, bindingKeystrokes[0...eventKeystrokes.length])
|
||||
false
|
||||
|
||||
normalizeCommandsByKeystrokes: (commandsByKeystrokes) ->
|
||||
normalizedCommandsByKeystrokes = {}
|
||||
for keystrokes, command of commandsByKeystrokes
|
||||
normalizedCommandsByKeystrokes[@normalizeKeystrokes(keystrokes)] = command
|
||||
normalizedCommandsByKeystrokes
|
||||
|
||||
normalizeKeystrokes: (keystrokes) ->
|
||||
normalizedKeystrokes = keystrokes.split(/\s+/).map (keystroke) =>
|
||||
@normalizeKeystroke(keystroke)
|
||||
normalizedKeystrokes.join(' ')
|
||||
|
||||
normalizeKeystroke: (keystroke) ->
|
||||
keys = @parser.parse(keystroke)
|
||||
modifiers = keys[0...-1]
|
||||
modifiers.sort()
|
||||
[modifiers..., _.last(keys)].join('-')
|
||||
57
src/app/buffer-change-operation.coffee
Normal file
57
src/app/buffer-change-operation.coffee
Normal file
@@ -0,0 +1,57 @@
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class BufferChangeOperation
|
||||
buffer: null
|
||||
oldRange: null
|
||||
oldText: null
|
||||
newRange: null
|
||||
newText: null
|
||||
|
||||
constructor: ({@buffer, @oldRange, @newText}) ->
|
||||
|
||||
do: ->
|
||||
@oldText = @buffer.getTextInRange(@oldRange)
|
||||
@newRange = @calculateNewRange(@oldRange, @newText)
|
||||
@changeBuffer
|
||||
oldRange: @oldRange
|
||||
newRange: @newRange
|
||||
oldText: @oldText
|
||||
newText: @newText
|
||||
|
||||
undo: ->
|
||||
@changeBuffer
|
||||
oldRange: @newRange
|
||||
newRange: @oldRange
|
||||
oldText: @newText
|
||||
newText: @oldText
|
||||
|
||||
changeBuffer: ({ oldRange, newRange, newText, oldText }) ->
|
||||
{ prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange)
|
||||
|
||||
newTextLines = newText.split('\n')
|
||||
if newTextLines.length == 1
|
||||
newTextLines = [prefix + newText + suffix]
|
||||
else
|
||||
lastLineIndex = newTextLines.length - 1
|
||||
newTextLines[0] = prefix + newTextLines[0]
|
||||
newTextLines[lastLineIndex] += suffix
|
||||
|
||||
@buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines)
|
||||
|
||||
event = { oldRange, newRange, oldText, newText }
|
||||
@buffer.trigger 'change', event
|
||||
anchor.handleBufferChange(event) for anchor in @buffer.getAnchors()
|
||||
@buffer.trigger 'update-anchors-after-change'
|
||||
newRange
|
||||
|
||||
calculateNewRange: (oldRange, newText) ->
|
||||
newRange = new Range(oldRange.start.copy(), oldRange.start.copy())
|
||||
newTextLines = newText.split('\n')
|
||||
if newTextLines.length == 1
|
||||
newRange.end.column += newText.length
|
||||
else
|
||||
lastLineIndex = newTextLines.length - 1
|
||||
newRange.end.row += lastLineIndex
|
||||
newRange.end.column = newTextLines[lastLineIndex].length
|
||||
newRange
|
||||
357
src/app/buffer.coffee
Normal file
357
src/app/buffer.coffee
Normal file
@@ -0,0 +1,357 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
File = require 'file'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
EventEmitter = require 'event-emitter'
|
||||
UndoManager = require 'undo-manager'
|
||||
BufferChangeOperation = require 'buffer-change-operation'
|
||||
Anchor = require 'anchor'
|
||||
AnchorRange = require 'anchor-range'
|
||||
|
||||
module.exports =
|
||||
class Buffer
|
||||
@idCounter = 1
|
||||
undoManager: null
|
||||
modified: null
|
||||
modifiedOnDisk: null
|
||||
lines: null
|
||||
file: null
|
||||
anchors: null
|
||||
anchorRanges: null
|
||||
refcount: 0
|
||||
|
||||
constructor: (path, @project) ->
|
||||
@id = @constructor.idCounter++
|
||||
@anchors = []
|
||||
@anchorRanges = []
|
||||
@lines = ['']
|
||||
|
||||
if path
|
||||
throw "Path '#{path}' does not exist" unless fs.exists(path)
|
||||
@setPath(path)
|
||||
@setText(fs.read(@getPath()))
|
||||
else
|
||||
@setText('')
|
||||
|
||||
@undoManager = new UndoManager(this)
|
||||
@modified = false
|
||||
|
||||
destroy: ->
|
||||
throw new Error("Destroying buffer twice with path '#{@getPath()}'") if @destroyed
|
||||
@file?.off()
|
||||
@destroyed = true
|
||||
@project?.removeBuffer(this)
|
||||
|
||||
retain: ->
|
||||
@refcount++
|
||||
this
|
||||
|
||||
release: ->
|
||||
@refcount--
|
||||
@destroy() if @refcount <= 0
|
||||
this
|
||||
|
||||
subscribeToFile: ->
|
||||
@file.on "contents-change", =>
|
||||
if @isModified()
|
||||
@modifiedOnDisk = true
|
||||
else
|
||||
@setText(fs.read(@file.getPath()))
|
||||
@modified = false
|
||||
|
||||
@file.on "remove", =>
|
||||
@file = null
|
||||
@trigger "path-change", this
|
||||
|
||||
@file.on "move", =>
|
||||
@trigger "path-change", this
|
||||
|
||||
reload: ->
|
||||
@setText(fs.read(@file.getPath()))
|
||||
@modified = false
|
||||
@modifiedOnDisk = false
|
||||
|
||||
getBaseName: ->
|
||||
@file?.getBaseName()
|
||||
|
||||
getPath: ->
|
||||
@file?.getPath()
|
||||
|
||||
setPath: (path) ->
|
||||
return if path == @getPath()
|
||||
|
||||
@file?.off()
|
||||
@file = new File(path)
|
||||
@subscribeToFile()
|
||||
@file.on "contents-change", =>
|
||||
if @isModified()
|
||||
@modifiedOnDisk = true
|
||||
@trigger "contents-change-on-disk"
|
||||
else
|
||||
@setText(fs.read(@file.getPath()))
|
||||
@modified = false
|
||||
@trigger "path-change", this
|
||||
|
||||
getExtension: ->
|
||||
if @getPath()
|
||||
@getPath().split('/').pop().split('.').pop()
|
||||
else
|
||||
null
|
||||
|
||||
getText: ->
|
||||
@lines.join('\n')
|
||||
|
||||
setText: (text) ->
|
||||
@change(@getRange(), text)
|
||||
|
||||
getRange: ->
|
||||
new Range([0, 0], [@getLastRow(), @getLastLine().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)])
|
||||
|
||||
getLineCount: ->
|
||||
@getLines().length
|
||||
|
||||
getLastRow: ->
|
||||
@getLines().length - 1
|
||||
|
||||
getLastLine: ->
|
||||
@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)
|
||||
operation = new BufferChangeOperation({buffer: this, oldRange, newText})
|
||||
@pushOperation(operation)
|
||||
|
||||
clipPosition: (position) ->
|
||||
{ row, column } = Point.fromObject(position)
|
||||
row = 0 if row < 0
|
||||
column = 0 if column < 0
|
||||
row = Math.min(@getLastRow(), row)
|
||||
column = Math.min(@lineLengthForRow(row), column)
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
prefixAndSuffixForRange: (range) ->
|
||||
prefix: @lines[range.start.row][0...range.start.column]
|
||||
suffix: @lines[range.end.row][range.end.column..]
|
||||
|
||||
replaceLines: (startRow, endRow, newLines) ->
|
||||
@lines[startRow..endRow] = newLines
|
||||
@modified = true
|
||||
|
||||
pushOperation: (operation, editSession) ->
|
||||
if @undoManager
|
||||
@undoManager.pushOperation(operation, editSession)
|
||||
else
|
||||
operation.do()
|
||||
|
||||
transact: (fn) ->
|
||||
@undoManager.transact(fn)
|
||||
|
||||
undo: (editSession) ->
|
||||
@undoManager.undo(editSession)
|
||||
|
||||
redo: (editSession) ->
|
||||
@undoManager.redo(editSession)
|
||||
|
||||
save: ->
|
||||
@saveAs(@getPath())
|
||||
|
||||
saveAs: (path) ->
|
||||
if not path then throw new Error("Can't save buffer with no file path")
|
||||
|
||||
@trigger 'before-save'
|
||||
fs.write path, @getText()
|
||||
@file?.updateMd5()
|
||||
@modified = false
|
||||
@modifiedOnDisk = false
|
||||
@setPath(path)
|
||||
@trigger 'after-save'
|
||||
|
||||
isInConflict: ->
|
||||
@isModified() and @isModifiedOnDisk()
|
||||
|
||||
isModifiedOnDisk: ->
|
||||
@modifiedOnDisk
|
||||
|
||||
isModified: ->
|
||||
@modified
|
||||
|
||||
getAnchors: -> new Array(@anchors...)
|
||||
|
||||
addAnchor: (options) ->
|
||||
anchor = new Anchor(this, options)
|
||||
@anchors.push(anchor)
|
||||
anchor
|
||||
|
||||
addAnchorAtPosition: (position, options) ->
|
||||
anchor = @addAnchor(options)
|
||||
anchor.setBufferPosition(position)
|
||||
anchor
|
||||
|
||||
addAnchorRange: (range, editSession) ->
|
||||
anchorRange = new AnchorRange(range, this, editSession)
|
||||
@anchorRanges.push(anchorRange)
|
||||
anchorRange
|
||||
|
||||
removeAnchor: (anchor) ->
|
||||
_.remove(@anchors, anchor)
|
||||
|
||||
removeAnchorRange: (anchorRange) ->
|
||||
_.remove(@anchorRanges, anchorRange)
|
||||
|
||||
matchesInCharacterRange: (regex, startIndex, endIndex) ->
|
||||
text = @getText()
|
||||
matches = []
|
||||
|
||||
regex.lastIndex = startIndex
|
||||
while match = regex.exec(text)
|
||||
matchLength = match[0].length
|
||||
matchStartIndex = match.index
|
||||
matchEndIndex = matchStartIndex + matchLength
|
||||
|
||||
if matchEndIndex > endIndex
|
||||
regex.lastIndex = 0
|
||||
if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex])
|
||||
submatch.index = matchStartIndex
|
||||
matches.push submatch
|
||||
break
|
||||
|
||||
matchEndIndex++ if matchLength is 0
|
||||
regex.lastIndex = matchEndIndex
|
||||
matches.push match
|
||||
|
||||
matches
|
||||
|
||||
scan: (regex, iterator) ->
|
||||
@scanInRange(regex, @getRange(), iterator)
|
||||
|
||||
scanInRange: (regex, range, iterator, reverse=false) ->
|
||||
range = Range.fromObject(range)
|
||||
global = regex.global
|
||||
regex = new RegExp(regex.source, 'gm')
|
||||
|
||||
startIndex = @characterIndexForPosition(range.start)
|
||||
endIndex = @characterIndexForPosition(range.end)
|
||||
|
||||
matches = @matchesInCharacterRange(regex, startIndex, endIndex)
|
||||
lengthDelta = 0
|
||||
|
||||
keepLooping = null
|
||||
replacementText = null
|
||||
stop = -> keepLooping = false
|
||||
replace = (text) -> replacementText = text
|
||||
|
||||
matches.reverse() if reverse
|
||||
for match in matches
|
||||
matchLength = match[0].length
|
||||
matchStartIndex = match.index
|
||||
matchEndIndex = matchStartIndex + matchLength
|
||||
|
||||
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
|
||||
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
|
||||
range = new Range(startPosition, endPosition)
|
||||
keepLooping = true
|
||||
replacementText = null
|
||||
iterator(match, range, { stop, replace })
|
||||
|
||||
if replacementText?
|
||||
@change(range, replacementText)
|
||||
lengthDelta += replacementText.length - matchLength unless reverse
|
||||
|
||||
break unless global and keepLooping
|
||||
|
||||
backwardsScanInRange: (regex, range, iterator) ->
|
||||
@scanInRange regex, range, iterator, true
|
||||
|
||||
isRowBlank: (row) ->
|
||||
not /\S/.test @lineForRow(row)
|
||||
|
||||
previousNonBlankRow: (startRow) ->
|
||||
return null if startRow == 0
|
||||
|
||||
startRow = Math.min(startRow, @getLastRow())
|
||||
for row in [(startRow - 1)..0]
|
||||
return row unless @isRowBlank(row)
|
||||
null
|
||||
|
||||
nextNonBlankRow: (startRow) ->
|
||||
lastRow = @getLastRow()
|
||||
if startRow < lastRow
|
||||
for row in [(startRow + 1)..lastRow]
|
||||
return row unless @isRowBlank(row)
|
||||
null
|
||||
|
||||
indentationForRow: (row) ->
|
||||
@lineForRow(row).match(/^\s*/)?[0].length
|
||||
|
||||
setIndentationForRow: (bufferRow, newLevel) ->
|
||||
currentLevel = @indentationForRow(bufferRow)
|
||||
indentString = [0...newLevel].map(-> ' ').join('')
|
||||
@change([[bufferRow, 0], [bufferRow, currentLevel]], indentString)
|
||||
|
||||
logLines: (start=0, end=@getLastRow())->
|
||||
for row in [start..end]
|
||||
line = @lineForRow(row)
|
||||
console.log row, line, line.length
|
||||
|
||||
_.extend(Buffer.prototype, EventEmitter)
|
||||
62
src/app/cursor-view.coffee
Normal file
62
src/app/cursor-view.coffee
Normal file
@@ -0,0 +1,62 @@
|
||||
{View} = require 'space-pen'
|
||||
Anchor = require 'anchor'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class CursorView extends View
|
||||
@content: ->
|
||||
@pre class: 'cursor idle', => @raw ' '
|
||||
|
||||
editor: null
|
||||
hidden: false
|
||||
|
||||
initialize: (@cursor, @editor) ->
|
||||
@cursor.on 'change-screen-position.cursor-view', (position, { bufferChange }) =>
|
||||
@updateAppearance()
|
||||
@removeIdleClassTemporarily() unless bufferChange
|
||||
@trigger 'cursor-move', bufferChange: bufferChange
|
||||
|
||||
@cursor.on 'destroy.cursor-view', => @remove()
|
||||
|
||||
afterAttach: (onDom) ->
|
||||
return unless onDom
|
||||
@updateAppearance()
|
||||
@editor.syncCursorAnimations()
|
||||
|
||||
remove: ->
|
||||
@editor.removeCursorView(this)
|
||||
@cursor.off('.cursor-view')
|
||||
super
|
||||
|
||||
updateAppearance: ->
|
||||
screenPosition = @getScreenPosition()
|
||||
pixelPosition = @editor.pixelPositionForScreenPosition(screenPosition)
|
||||
@css(pixelPosition)
|
||||
|
||||
if @cursor == @editor.getLastCursor()
|
||||
@editor.scrollTo(pixelPosition)
|
||||
|
||||
if @editor.isFoldedAtScreenRow(screenPosition.row)
|
||||
@hide() unless @hidden
|
||||
@hidden = true
|
||||
else
|
||||
@show() if @hidden
|
||||
@hidden = false
|
||||
|
||||
getBufferPosition: ->
|
||||
@cursor.getBufferPosition()
|
||||
|
||||
getScreenPosition: ->
|
||||
@cursor.getScreenPosition()
|
||||
|
||||
removeIdleClassTemporarily: ->
|
||||
@removeClass 'idle'
|
||||
window.clearTimeout(@idleTimeout) if @idleTimeout
|
||||
@idleTimeout = window.setTimeout (=> @addClass 'idle'), 200
|
||||
|
||||
resetCursorAnimation: ->
|
||||
window.clearTimeout(@idleTimeout) if @idleTimeout
|
||||
@removeClass 'idle'
|
||||
_.defer => @addClass 'idle'
|
||||
162
src/app/cursor.coffee
Normal file
162
src/app/cursor.coffee
Normal file
@@ -0,0 +1,162 @@
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
Anchor = require 'anchor'
|
||||
EventEmitter = require 'event-emitter'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Cursor
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
wordRegex: /(\w+)|([^\w\s]+)/g
|
||||
|
||||
constructor: ({@editSession, screenPosition, bufferPosition}) ->
|
||||
@anchor = @editSession.addAnchor(strong: true)
|
||||
@anchor.on 'change-screen-position', (args...) => @trigger 'change-screen-position', args...
|
||||
@setScreenPosition(screenPosition) if screenPosition
|
||||
@setBufferPosition(bufferPosition) if bufferPosition
|
||||
|
||||
destroy: ->
|
||||
@anchor.destroy()
|
||||
@editSession.removeCursor(this)
|
||||
@trigger 'destroy'
|
||||
|
||||
setScreenPosition: (screenPosition, options) ->
|
||||
@goalColumn = null
|
||||
@clearSelection()
|
||||
@anchor.setScreenPosition(screenPosition, options)
|
||||
|
||||
getScreenPosition: ->
|
||||
@anchor.getScreenPosition()
|
||||
|
||||
setBufferPosition: (bufferPosition, options) ->
|
||||
@goalColumn = null
|
||||
@clearSelection()
|
||||
@anchor.setBufferPosition(bufferPosition, options)
|
||||
|
||||
getBufferPosition: ->
|
||||
@anchor.getBufferPosition()
|
||||
|
||||
clearSelection: ->
|
||||
if @selection
|
||||
@selection.clear() unless @selection.retainSelection
|
||||
|
||||
getScreenRow: ->
|
||||
@getScreenPosition().row
|
||||
|
||||
getBufferRow: ->
|
||||
@getBufferPosition().row
|
||||
|
||||
getCurrentBufferLine: ->
|
||||
@editSession.lineForBufferRow(@getBufferRow())
|
||||
|
||||
refreshScreenPosition: ->
|
||||
@anchor.refreshScreenPosition()
|
||||
|
||||
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
|
||||
|
||||
moveLeft: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
[row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
|
||||
@setScreenPosition({row, column})
|
||||
|
||||
moveRight: ->
|
||||
{ row, column } = @getScreenPosition()
|
||||
@setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
|
||||
|
||||
moveToTop: ->
|
||||
@setBufferPosition([0,0])
|
||||
|
||||
moveToBottom: ->
|
||||
@setBufferPosition(@editSession.getEofBufferPosition())
|
||||
|
||||
moveToBeginningOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), 0])
|
||||
|
||||
moveToFirstCharacterOfLine: ->
|
||||
position = @getBufferPosition()
|
||||
range = @editSession.bufferRangeForBufferRow(position.row)
|
||||
newPosition = null
|
||||
@editSession.scanInRange /^\s*/, range, (match, matchRange) =>
|
||||
newPosition = matchRange.end
|
||||
return unless newPosition
|
||||
newPosition = [position.row, 0] if newPosition.isEqual(position)
|
||||
@setBufferPosition(newPosition)
|
||||
|
||||
moveToEndOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), Infinity])
|
||||
|
||||
moveToBeginningOfWord: ->
|
||||
@setBufferPosition(@getBeginningOfCurrentWordBufferPosition())
|
||||
|
||||
moveToEndOfWord: ->
|
||||
@setBufferPosition(@getEndOfCurrentWordBufferPosition())
|
||||
|
||||
moveToNextWord: ->
|
||||
@setBufferPosition(@getBeginningOfNextWordBufferPosition())
|
||||
|
||||
getBeginningOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowPrevious = options.allowPrevious ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
previousRow = Math.max(0, currentBufferPosition.row - 1)
|
||||
previousLinesRange = [[previousRow, 0], currentBufferPosition]
|
||||
|
||||
beginningOfWordPosition = currentBufferPosition
|
||||
@editSession.backwardsScanInRange @wordRegex, previousLinesRange, (match, matchRange, { stop }) =>
|
||||
if matchRange.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
|
||||
beginningOfWordPosition = matchRange.start
|
||||
stop()
|
||||
beginningOfWordPosition
|
||||
|
||||
getEndOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowNext = options.allowNext ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
range = [currentBufferPosition, @editSession.getEofBufferPosition()]
|
||||
|
||||
endOfWordPosition = null
|
||||
@editSession.scanInRange @wordRegex, range, (match, matchRange, { stop }) =>
|
||||
endOfWordPosition = matchRange.end
|
||||
if not allowNext and matchRange.start.isGreaterThan(currentBufferPosition)
|
||||
endOfWordPosition = currentBufferPosition
|
||||
stop()
|
||||
endOfWordPosition
|
||||
|
||||
getBeginningOfNextWordBufferPosition: ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
eofBufferPosition = @editSession.getEofBufferPosition()
|
||||
range = [currentBufferPosition, eofBufferPosition]
|
||||
|
||||
nextWordPosition = eofBufferPosition
|
||||
@editSession.scanInRange @wordRegex, range, (match, matchRange, { stop }) =>
|
||||
if matchRange.start.isGreaterThan(currentBufferPosition)
|
||||
nextWordPosition = matchRange.start
|
||||
stop()
|
||||
nextWordPosition
|
||||
|
||||
getCurrentWordBufferRange: ->
|
||||
new Range(@getBeginningOfCurrentWordBufferPosition(allowPrevious: false), @getEndOfCurrentWordBufferPosition(allowNext: false))
|
||||
|
||||
getCurrentLineBufferRange: ->
|
||||
@editSession.bufferRangeForBufferRow(@getBufferRow())
|
||||
|
||||
getCurrentWordPrefix: ->
|
||||
@editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
|
||||
|
||||
isAtBeginningOfLine: ->
|
||||
@getBufferPosition().column == 0
|
||||
|
||||
isAtEndOfLine: ->
|
||||
@getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
|
||||
|
||||
_.extend Cursor.prototype, EventEmitter
|
||||
38
src/app/directory.coffee
Normal file
38
src/app/directory.coffee
Normal file
@@ -0,0 +1,38 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
File = require 'file'
|
||||
EventEmitter = require 'event-emitter'
|
||||
|
||||
module.exports =
|
||||
class Directory
|
||||
path: null
|
||||
|
||||
constructor: (@path) ->
|
||||
|
||||
getBaseName: ->
|
||||
fs.base(@path) + '/'
|
||||
|
||||
getEntries: ->
|
||||
directories = []
|
||||
files = []
|
||||
for path in fs.list(@path)
|
||||
if fs.isDirectory(path)
|
||||
directories.push(new Directory(path))
|
||||
else
|
||||
files.push(new File(path))
|
||||
directories.concat(files)
|
||||
|
||||
afterSubscribe: ->
|
||||
@subscribeToNativeChangeEvents() if @subscriptionCount() == 1
|
||||
|
||||
afterUnsubscribe: ->
|
||||
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
|
||||
|
||||
subscribeToNativeChangeEvents: ->
|
||||
@watchId = $native.watchPath @path, (eventType) =>
|
||||
@trigger "contents-change" if eventType is "contents-change"
|
||||
|
||||
unsubscribeFromNativeChangeEvents: ->
|
||||
$native.unwatchPath(@path, @watchId)
|
||||
|
||||
_.extend Directory.prototype, EventEmitter
|
||||
281
src/app/display-buffer.coffee
Normal file
281
src/app/display-buffer.coffee
Normal file
@@ -0,0 +1,281 @@
|
||||
_ = require 'underscore'
|
||||
TokenizedBuffer = require 'tokenized-buffer'
|
||||
LineMap = require 'line-map'
|
||||
Point = require 'point'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Range = require 'range'
|
||||
Fold = require 'fold'
|
||||
ScreenLine = require 'screen-line'
|
||||
Token = require 'token'
|
||||
|
||||
module.exports =
|
||||
class DisplayBuffer
|
||||
@idCounter: 1
|
||||
lineMap: null
|
||||
languageMode: null
|
||||
tokenizedBuffer: null
|
||||
activeFolds: null
|
||||
foldsById: null
|
||||
lastTokenizedBufferChangeEvent: null
|
||||
|
||||
constructor: (@buffer, options={}) ->
|
||||
@id = @constructor.idCounter++
|
||||
options.tabText ?= ' '
|
||||
@languageMode = options.languageMode
|
||||
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
|
||||
@softWrapColumn = options.softWrapColumn ? Infinity
|
||||
@activeFolds = {}
|
||||
@foldsById = {}
|
||||
@buildLineMap()
|
||||
@tokenizedBuffer.on 'change', (e) => @lastTokenizedBufferChangeEvent = e
|
||||
@buffer.on "change.displayBuffer#{@id}", (e) => @handleBufferChange(e)
|
||||
|
||||
buildLineMap: ->
|
||||
@lineMap = new LineMap
|
||||
@lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
|
||||
|
||||
setSoftWrapColumn: (@softWrapColumn) ->
|
||||
oldRange = @rangeForAllLines()
|
||||
@buildLineMap()
|
||||
newRange = @rangeForAllLines()
|
||||
@trigger 'change', { oldRange, newRange, lineNumbersChanged: true }
|
||||
|
||||
lineForRow: (row) ->
|
||||
@lineMap.lineForScreenRow(row)
|
||||
|
||||
linesForRows: (startRow, endRow) ->
|
||||
@lineMap.linesForScreenRows(startRow, endRow)
|
||||
|
||||
getLines: ->
|
||||
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
|
||||
|
||||
bufferRowsForScreenRows: (startRow, endRow) ->
|
||||
@lineMap.bufferRowsForScreenRows(startRow, endRow)
|
||||
|
||||
foldAll: ->
|
||||
for currentRow in [0..@buffer.getLastRow()]
|
||||
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow?
|
||||
|
||||
@createFold(startRow, endRow)
|
||||
|
||||
unfoldAll: ->
|
||||
for row in [@buffer.getLastRow()..0]
|
||||
@activeFolds[row]?.forEach (fold) => @destroyFold(fold)
|
||||
|
||||
foldBufferRow: (bufferRow) ->
|
||||
for currentRow in [bufferRow..0]
|
||||
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow? and startRow <= bufferRow <= endRow
|
||||
fold = @largestFoldStartingAtBufferRow(startRow)
|
||||
continue if fold
|
||||
|
||||
@createFold(startRow, endRow)
|
||||
|
||||
return
|
||||
|
||||
unfoldBufferRow: (bufferRow) ->
|
||||
@largestFoldContainingBufferRow(bufferRow)?.destroy()
|
||||
|
||||
createFold: (startRow, endRow) ->
|
||||
return fold if fold = @foldFor(startRow, endRow)
|
||||
fold = new Fold(this, startRow, endRow)
|
||||
@registerFold(fold)
|
||||
|
||||
unless @isFoldContainedByActiveFold(fold)
|
||||
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
|
||||
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
|
||||
lines = @buildLineForBufferRow(startRow)
|
||||
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
|
||||
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
|
||||
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
|
||||
|
||||
fold
|
||||
|
||||
isFoldContainedByActiveFold: (fold) ->
|
||||
for row, folds of @activeFolds
|
||||
for otherFold in folds
|
||||
return otherFold if fold != otherFold and fold.isContainedByFold(otherFold)
|
||||
|
||||
foldFor: (startRow, endRow) ->
|
||||
_.find @activeFolds[startRow] ? [], (fold) ->
|
||||
fold.startRow == startRow and fold.endRow == endRow
|
||||
|
||||
destroyFold: (fold) ->
|
||||
@unregisterFold(fold.startRow, fold)
|
||||
|
||||
unless @isFoldContainedByActiveFold(fold)
|
||||
{ startRow, endRow } = fold
|
||||
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
|
||||
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
lines = @buildLinesForBufferRows(startRow, endRow)
|
||||
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
|
||||
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
|
||||
|
||||
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
|
||||
|
||||
destroyFoldsContainingBufferRow: (bufferRow) ->
|
||||
for row, folds of @activeFolds
|
||||
for fold in new Array(folds...)
|
||||
fold.destroy() if fold.getBufferRange().containsRow(bufferRow)
|
||||
|
||||
registerFold: (fold) ->
|
||||
@activeFolds[fold.startRow] ?= []
|
||||
@activeFolds[fold.startRow].push(fold)
|
||||
@foldsById[fold.id] = fold
|
||||
|
||||
unregisterFold: (bufferRow, fold) ->
|
||||
folds = @activeFolds[bufferRow]
|
||||
_.remove(folds, fold)
|
||||
delete @foldsById[fold.id]
|
||||
delete @activeFolds[bufferRow] if folds.length == 0
|
||||
|
||||
largestFoldStartingAtBufferRow: (bufferRow) ->
|
||||
return unless folds = @activeFolds[bufferRow]
|
||||
(folds.sort (a, b) -> b.endRow - a.endRow)[0]
|
||||
|
||||
largestFoldStartingAtScreenRow: (screenRow) ->
|
||||
@largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
|
||||
|
||||
largestFoldContainingBufferRow: (bufferRow) ->
|
||||
largestFold = null
|
||||
for currentBufferRow in [bufferRow..0]
|
||||
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
|
||||
largestFold = fold if fold.endRow >= bufferRow
|
||||
largestFold
|
||||
|
||||
screenLineRangeForBufferRange: (bufferRange) ->
|
||||
@expandScreenRangeToLineEnds(
|
||||
@lineMap.screenRangeForBufferRange(
|
||||
@expandBufferRangeToLineEnds(bufferRange)))
|
||||
|
||||
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()
|
||||
|
||||
getLastRow: ->
|
||||
@lineCount() - 1
|
||||
|
||||
maxLineLength: ->
|
||||
@lineMap.maxScreenLineLength()
|
||||
|
||||
screenPositionForBufferPosition: (position, options) ->
|
||||
@lineMap.screenPositionForBufferPosition(position, options)
|
||||
|
||||
bufferPositionForScreenPosition: (position, options) ->
|
||||
@lineMap.bufferPositionForScreenPosition(position, options)
|
||||
|
||||
stateForScreenRow: (screenRow) ->
|
||||
@tokenizedBuffer.stackForRow(screenRow)
|
||||
|
||||
clipScreenPosition: (position, options) ->
|
||||
@lineMap.clipScreenPosition(position, options)
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds
|
||||
allFolds.push(folds...) for row, folds of @activeFolds
|
||||
fold.handleBufferChange(e) for fold in allFolds
|
||||
|
||||
@handleTokenizedBufferChange(@lastTokenizedBufferChangeEvent)
|
||||
|
||||
handleTokenizedBufferChange: (e) ->
|
||||
newRange = e.newRange.copy()
|
||||
newRange.start.row = @bufferRowForScreenRow(@screenRowForBufferRow(newRange.start.row))
|
||||
|
||||
oldScreenRange = @screenLineRangeForBufferRange(e.oldRange)
|
||||
|
||||
newScreenLines = @buildLinesForBufferRows(newRange.start.row, newRange.end.row)
|
||||
@lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines
|
||||
newScreenRange = @screenLineRangeForBufferRange(newRange)
|
||||
|
||||
@trigger 'change',
|
||||
oldRange: oldScreenRange
|
||||
newRange: newScreenRange
|
||||
bufferChanged: true
|
||||
lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange)
|
||||
|
||||
buildLineForBufferRow: (bufferRow) ->
|
||||
@buildLinesForBufferRows(bufferRow, bufferRow)
|
||||
|
||||
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
|
||||
lineFragments = []
|
||||
startBufferColumn = null
|
||||
currentBufferRow = startBufferRow
|
||||
currentScreenLineLength = 0
|
||||
|
||||
startBufferColumn = 0
|
||||
while currentBufferRow <= endBufferRow
|
||||
screenLine = @tokenizedBuffer.lineForScreenRow(currentBufferRow)
|
||||
screenLine.foldable = @languageMode.doesBufferRowStartFold(currentBufferRow)
|
||||
|
||||
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
|
||||
screenLine = screenLine.copy()
|
||||
screenLine.fold = fold
|
||||
screenLine.bufferDelta = fold.getBufferDelta()
|
||||
lineFragments.push(screenLine)
|
||||
currentBufferRow = fold.endRow + 1
|
||||
continue
|
||||
|
||||
startBufferColumn ?= 0
|
||||
screenLine = screenLine.splitAt(startBufferColumn)[1] if startBufferColumn > 0
|
||||
wrapScreenColumn = @findWrapColumn(screenLine.text, @softWrapColumn)
|
||||
if wrapScreenColumn?
|
||||
screenLine = screenLine.splitAt(wrapScreenColumn)[0]
|
||||
screenLine.screenDelta = new Point(1, 0)
|
||||
startBufferColumn += wrapScreenColumn
|
||||
else
|
||||
currentBufferRow++
|
||||
startBufferColumn = 0
|
||||
|
||||
lineFragments.push(screenLine)
|
||||
|
||||
lineFragments
|
||||
|
||||
findWrapColumn: (line, softWrapColumn) ->
|
||||
return unless line.length > softWrapColumn
|
||||
|
||||
if /\s/.test(line[softWrapColumn])
|
||||
# search forward for the start of a word past the boundary
|
||||
for column in [softWrapColumn..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 [softWrapColumn..0]
|
||||
return column + 1 if /\s/.test(line[column])
|
||||
return softWrapColumn
|
||||
|
||||
expandScreenRangeToLineEnds: (screenRange) ->
|
||||
screenRange = Range.fromObject(screenRange)
|
||||
{ start, end } = screenRange
|
||||
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
|
||||
|
||||
expandBufferRangeToLineEnds: (bufferRange) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
{ start, end } = bufferRange
|
||||
new Range([start.row, 0], [end.row, Infinity])
|
||||
|
||||
rangeForAllLines: ->
|
||||
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
|
||||
|
||||
destroy: ->
|
||||
@tokenizedBuffer.destroy()
|
||||
@buffer.off ".displayBuffer#{@id}"
|
||||
|
||||
logLines: (start, end) ->
|
||||
@lineMap.logLines(start, end)
|
||||
|
||||
_.extend DisplayBuffer.prototype, EventEmitter
|
||||
545
src/app/edit-session.coffee
Normal file
545
src/app/edit-session.coffee
Normal file
@@ -0,0 +1,545 @@
|
||||
Point = require 'point'
|
||||
Buffer = require 'buffer'
|
||||
Anchor = require 'anchor'
|
||||
LanguageMode = require 'language-mode'
|
||||
DisplayBuffer = require 'display-buffer'
|
||||
Cursor = require 'cursor'
|
||||
Selection = require 'selection'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Range = require 'range'
|
||||
AnchorRange = require 'anchor-range'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class EditSession
|
||||
@idCounter: 1
|
||||
|
||||
@deserialize: (state, project) ->
|
||||
session = project.buildEditSessionForPath(state.buffer)
|
||||
session.setScrollTop(state.scrollTop)
|
||||
session.setScrollLeft(state.scrollLeft)
|
||||
session.setCursorScreenPosition(state.cursorScreenPosition)
|
||||
session
|
||||
|
||||
scrollTop: 0
|
||||
scrollLeft: 0
|
||||
languageMode: null
|
||||
displayBuffer: null
|
||||
anchors: null
|
||||
anchorRanges: null
|
||||
cursors: null
|
||||
selections: null
|
||||
autoIndent: false # TODO: re-enabled auto-indent after fixing the rest of tokenization
|
||||
softTabs: true
|
||||
softWrap: false
|
||||
|
||||
constructor: ({@project, @buffer, @tabText, @autoIndent, @softTabs, @softWrap}) ->
|
||||
@id = @constructor.idCounter++
|
||||
@softTabs ?= true
|
||||
@languageMode = new LanguageMode(this, @buffer.getExtension())
|
||||
@displayBuffer = new DisplayBuffer(@buffer, { @languageMode, @tabText })
|
||||
@tokenizedBuffer = @displayBuffer.tokenizedBuffer
|
||||
@anchors = []
|
||||
@anchorRanges = []
|
||||
@cursors = []
|
||||
@selections = []
|
||||
@addCursorAtScreenPosition([0, 0])
|
||||
|
||||
@buffer.retain()
|
||||
@buffer.on "path-change.edit-session-#{@id}", =>
|
||||
@trigger "buffer-path-change"
|
||||
|
||||
@buffer.on "contents-change-on-disk.edit-session-#{@id}", =>
|
||||
@trigger "buffer-contents-change-on-disk"
|
||||
|
||||
@buffer.on "update-anchors-after-change.edit-session-#{@id}", =>
|
||||
@mergeCursors()
|
||||
|
||||
@displayBuffer.on "change.edit-session-#{@id}", (e) =>
|
||||
@trigger 'screen-lines-change', e
|
||||
unless e.bufferChanged
|
||||
anchor.refreshScreenPosition() for anchor in @getAnchors()
|
||||
|
||||
destroy: ->
|
||||
throw new Error("Edit session already destroyed") if @destroyed
|
||||
@destroyed = true
|
||||
|
||||
@buffer.off ".edit-session-#{@id}"
|
||||
@buffer.release()
|
||||
@displayBuffer.off ".edit-session-#{@id}"
|
||||
@displayBuffer.destroy()
|
||||
@project.removeEditSession(this)
|
||||
anchor.destroy() for anchor in @getAnchors()
|
||||
anchorRange.destroy() for anchorRange in @getAnchorRanges()
|
||||
|
||||
serialize: ->
|
||||
buffer: @buffer.getPath()
|
||||
scrollTop: @getScrollTop()
|
||||
scrollLeft: @getScrollLeft()
|
||||
cursorScreenPosition: @getCursorScreenPosition().serialize()
|
||||
|
||||
copy: ->
|
||||
EditSession.deserialize(@serialize(), @project)
|
||||
|
||||
isEqual: (other) ->
|
||||
return false unless other instanceof EditSession
|
||||
@buffer == other.buffer and
|
||||
@scrollTop == other.getScrollTop() and
|
||||
@scrollLeft == other.getScrollLeft() and
|
||||
@getCursorScreenPosition().isEqual(other.getCursorScreenPosition())
|
||||
|
||||
setScrollTop: (@scrollTop) ->
|
||||
getScrollTop: -> @scrollTop
|
||||
|
||||
setScrollLeft: (@scrollLeft) ->
|
||||
getScrollLeft: -> @scrollLeft
|
||||
|
||||
setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn)
|
||||
setAutoIndent: (@autoIndent) ->
|
||||
setSoftTabs: (@softTabs) ->
|
||||
|
||||
getSoftWrap: -> @softWrap
|
||||
setSoftWrap: (@softWrap) ->
|
||||
|
||||
clipBufferPosition: (bufferPosition) ->
|
||||
@buffer.clipPosition(bufferPosition)
|
||||
|
||||
getFileExtension: -> @buffer.getExtension()
|
||||
getPath: -> @buffer.getPath()
|
||||
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
|
||||
nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)
|
||||
indentationForBufferRow: (bufferRow) -> @buffer.indentationForRow(bufferRow)
|
||||
getEofBufferPosition: -> @buffer.getEofPosition()
|
||||
getLastBufferRow: -> @buffer.getLastRow()
|
||||
bufferRangeForBufferRow: (row) -> @buffer.rangeForRow(row)
|
||||
lineForBufferRow: (row) -> @buffer.lineForRow(row)
|
||||
scanInRange: (args...) -> @buffer.scanInRange(args...)
|
||||
backwardsScanInRange: (args...) -> @buffer.backwardsScanInRange(args...)
|
||||
|
||||
screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)
|
||||
bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)
|
||||
screenRangeForBufferRange: (range) -> @displayBuffer.screenRangeForBufferRange(range)
|
||||
bufferRangeForScreenRange: (range) -> @displayBuffer.bufferRangeForScreenRange(range)
|
||||
clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options)
|
||||
lineForScreenRow: (row) -> @displayBuffer.lineForRow(row)
|
||||
linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end)
|
||||
stateForScreenRow: (screenRow) -> @displayBuffer.stateForScreenRow(screenRow)
|
||||
screenLineCount: -> @displayBuffer.lineCount()
|
||||
maxScreenLineLength: -> @displayBuffer.maxLineLength()
|
||||
getLastScreenRow: -> @displayBuffer.getLastRow()
|
||||
bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
|
||||
logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
|
||||
|
||||
insertText: (text, options) ->
|
||||
@mutateSelectedText (selection) -> selection.insertText(text, options)
|
||||
|
||||
insertNewline: ->
|
||||
@insertText('\n')
|
||||
|
||||
insertNewlineBelow: ->
|
||||
@moveCursorToEndOfLine()
|
||||
@insertNewline()
|
||||
|
||||
indent: ->
|
||||
currentRow = @getCursorBufferPosition().row
|
||||
if @getSelection().isEmpty()
|
||||
if @softTabs
|
||||
@insertText(@tabText)
|
||||
else
|
||||
@insertText('\t')
|
||||
else
|
||||
@indentSelectedRows()
|
||||
|
||||
backspace: ->
|
||||
@mutateSelectedText (selection) -> selection.backspace()
|
||||
|
||||
backspaceToBeginningOfWord: ->
|
||||
@mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord()
|
||||
|
||||
delete: ->
|
||||
@mutateSelectedText (selection) -> selection.delete()
|
||||
|
||||
deleteToEndOfWord: ->
|
||||
@mutateSelectedText (selection) -> selection.deleteToEndOfWord()
|
||||
|
||||
indentSelectedRows: ->
|
||||
@mutateSelectedText (selection) -> selection.indentSelectedRows()
|
||||
|
||||
outdentSelectedRows: ->
|
||||
@mutateSelectedText (selection) -> selection.outdentSelectedRows()
|
||||
|
||||
toggleLineCommentsInSelection: ->
|
||||
@mutateSelectedText (selection) -> selection.toggleLineComments()
|
||||
|
||||
cutToEndOfLine: ->
|
||||
maintainPasteboard = false
|
||||
@mutateSelectedText (selection) ->
|
||||
selection.cutToEndOfLine(maintainPasteboard)
|
||||
maintainPasteboard = true
|
||||
|
||||
cutSelectedText: ->
|
||||
maintainPasteboard = false
|
||||
@mutateSelectedText (selection) ->
|
||||
selection.cut(maintainPasteboard)
|
||||
maintainPasteboard = true
|
||||
|
||||
copySelectedText: ->
|
||||
maintainPasteboard = false
|
||||
for selection in @getSelections()
|
||||
selection.copy(maintainPasteboard)
|
||||
maintainPasteboard = true
|
||||
|
||||
pasteText: ->
|
||||
@insertText($native.readFromPasteboard())
|
||||
|
||||
undo: ->
|
||||
@buffer.undo(this)
|
||||
|
||||
redo: ->
|
||||
@buffer.redo(this)
|
||||
|
||||
foldAll: ->
|
||||
@displayBuffer.foldAll()
|
||||
|
||||
unfoldAll: ->
|
||||
@displayBuffer.unfoldAll()
|
||||
|
||||
foldCurrentRow: ->
|
||||
bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
|
||||
@foldBufferRow(bufferRow)
|
||||
|
||||
foldBufferRow: (bufferRow) ->
|
||||
@displayBuffer.foldBufferRow(bufferRow)
|
||||
|
||||
unfoldCurrentRow: ->
|
||||
bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
|
||||
@unfoldBufferRow(bufferRow)
|
||||
|
||||
unfoldBufferRow: (bufferRow) ->
|
||||
@displayBuffer.unfoldBufferRow(bufferRow)
|
||||
|
||||
foldSelection: ->
|
||||
selection.fold() for selection in @getSelections()
|
||||
|
||||
createFold: (startRow, endRow) ->
|
||||
@displayBuffer.createFold(startRow, endRow)
|
||||
|
||||
destroyFoldsContainingBufferRow: (bufferRow) ->
|
||||
@displayBuffer.destroyFoldsContainingBufferRow(bufferRow)
|
||||
|
||||
destroyFoldsIntersectingBufferRange: (bufferRange) ->
|
||||
for row in [bufferRange.start.row..bufferRange.end.row]
|
||||
@destroyFoldsContainingBufferRow(row)
|
||||
|
||||
destroyFold: (foldId) ->
|
||||
fold = @displayBuffer.foldsById[foldId]
|
||||
fold.destroy()
|
||||
@setCursorBufferPosition([fold.startRow, 0])
|
||||
|
||||
isFoldedAtScreenRow: (screenRow) ->
|
||||
@lineForScreenRow(screenRow).fold?
|
||||
|
||||
largestFoldContainingBufferRow: (bufferRow) ->
|
||||
@displayBuffer.largestFoldContainingBufferRow(bufferRow)
|
||||
|
||||
largestFoldStartingAtScreenRow: (screenRow) ->
|
||||
@displayBuffer.largestFoldStartingAtScreenRow(screenRow)
|
||||
|
||||
autoIndentBufferRows: (startRow, endRow) ->
|
||||
@languageMode.autoIndentBufferRows(startRow, endRow)
|
||||
|
||||
autoIndentBufferRow: (bufferRow) ->
|
||||
@languageMode.autoIndentBufferRow(bufferRow)
|
||||
|
||||
autoIncreaseIndentForBufferRow: (bufferRow) ->
|
||||
@languageMode.autoIncreaseIndentForBufferRow(bufferRow)
|
||||
|
||||
autoDecreaseIndentForRow: (bufferRow) ->
|
||||
@languageMode.autoDecreaseIndentForBufferRow(bufferRow)
|
||||
|
||||
toggleLineCommentsInRange: (range) ->
|
||||
@languageMode.toggleLineCommentsInRange(range)
|
||||
|
||||
mutateSelectedText: (fn) ->
|
||||
@transact => fn(selection) for selection in @getSelections()
|
||||
|
||||
transact: (fn) ->
|
||||
@buffer.transact =>
|
||||
oldSelectedRanges = @getSelectedBufferRanges()
|
||||
@pushOperation
|
||||
undo: (editSession) ->
|
||||
editSession?.setSelectedBufferRanges(oldSelectedRanges)
|
||||
|
||||
fn()
|
||||
newSelectedRanges = @getSelectedBufferRanges()
|
||||
@pushOperation
|
||||
redo: (editSession) ->
|
||||
editSession?.setSelectedBufferRanges(newSelectedRanges)
|
||||
|
||||
pushOperation: (operation) ->
|
||||
@buffer.pushOperation(operation, this)
|
||||
|
||||
getAnchors: ->
|
||||
new Array(@anchors...)
|
||||
|
||||
getAnchorRanges: ->
|
||||
new Array(@anchorRanges...)
|
||||
|
||||
addAnchor: (options={}) ->
|
||||
anchor = @buffer.addAnchor(_.extend({editSession: this}, options))
|
||||
@anchors.push(anchor)
|
||||
anchor
|
||||
|
||||
addAnchorAtBufferPosition: (bufferPosition, options) ->
|
||||
anchor = @addAnchor(options)
|
||||
anchor.setBufferPosition(bufferPosition)
|
||||
anchor
|
||||
|
||||
addAnchorRange: (range) ->
|
||||
anchorRange = @buffer.addAnchorRange(range, this)
|
||||
@anchorRanges.push(anchorRange)
|
||||
anchorRange
|
||||
|
||||
removeAnchor: (anchor) ->
|
||||
_.remove(@anchors, anchor)
|
||||
|
||||
removeAnchorRange: (anchorRange) ->
|
||||
_.remove(@anchorRanges, anchorRange)
|
||||
|
||||
hasMultipleCursors: ->
|
||||
@getCursors().length > 1
|
||||
|
||||
getCursors: -> new Array(@cursors...)
|
||||
|
||||
getCursor: (index=0) ->
|
||||
@cursors[index]
|
||||
|
||||
getLastCursor: ->
|
||||
_.last(@cursors)
|
||||
|
||||
addCursorAtScreenPosition: (screenPosition) ->
|
||||
@addCursor(new Cursor(editSession: this, screenPosition: screenPosition))
|
||||
|
||||
addCursorAtBufferPosition: (bufferPosition) ->
|
||||
@addCursor(new Cursor(editSession: this, bufferPosition: bufferPosition))
|
||||
|
||||
addCursor: (cursor=new Cursor(editSession: this, screenPosition: [0,0])) ->
|
||||
@cursors.push(cursor)
|
||||
@trigger 'add-cursor', cursor
|
||||
@addSelectionForCursor(cursor)
|
||||
cursor
|
||||
|
||||
removeCursor: (cursor) ->
|
||||
_.remove(@cursors, cursor)
|
||||
|
||||
addSelectionForCursor: (cursor) ->
|
||||
selection = new Selection(editSession: this, cursor: cursor)
|
||||
@selections.push(selection)
|
||||
@trigger 'add-selection', selection
|
||||
selection
|
||||
|
||||
addSelectionForBufferRange: (bufferRange, options={}) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
@destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
||||
@addCursor().selection.setBufferRange(bufferRange, options)
|
||||
@mergeIntersectingSelections()
|
||||
|
||||
setSelectedBufferRange: (bufferRange, options) ->
|
||||
@setSelectedBufferRanges([bufferRange], options)
|
||||
|
||||
setSelectedBufferRanges: (bufferRanges, options={}) ->
|
||||
throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
|
||||
|
||||
selections = @getSelections()
|
||||
selection.destroy() for selection in selections[bufferRanges.length...]
|
||||
|
||||
for bufferRange, i in bufferRanges
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
if selections[i]
|
||||
selections[i].setBufferRange(bufferRange, options)
|
||||
else
|
||||
@addSelectionForBufferRange(bufferRange, options)
|
||||
@mergeIntersectingSelections(options)
|
||||
|
||||
removeSelection: (selection) ->
|
||||
_.remove(@selections, selection)
|
||||
|
||||
clearSelections: ->
|
||||
lastSelection = @getLastSelection()
|
||||
for selection in @getSelections() when selection != lastSelection
|
||||
selection.destroy()
|
||||
lastSelection.clear()
|
||||
|
||||
clearAllSelections: ->
|
||||
selection.destroy() for selection in @getSelections()
|
||||
|
||||
getSelections: -> new Array(@selections...)
|
||||
|
||||
getSelection: (index) ->
|
||||
index ?= @selections.length - 1
|
||||
@selections[index]
|
||||
|
||||
getLastSelection: ->
|
||||
_.last(@selections)
|
||||
|
||||
getSelectionsOrderedByBufferPosition: ->
|
||||
@getSelections().sort (a, b) ->
|
||||
aRange = a.getBufferRange()
|
||||
bRange = b.getBufferRange()
|
||||
aRange.end.compare(bRange.end)
|
||||
|
||||
getLastSelectionInBuffer: ->
|
||||
_.last(@getSelectionsOrderedByBufferPosition())
|
||||
|
||||
selectionIntersectsBufferRange: (bufferRange) ->
|
||||
_.any @getSelections(), (selection) ->
|
||||
selection.intersectsBufferRange(bufferRange)
|
||||
|
||||
setCursorScreenPosition: (position) ->
|
||||
@moveCursors (cursor) -> cursor.setScreenPosition(position)
|
||||
|
||||
getCursorScreenPosition: ->
|
||||
@getLastCursor().getScreenPosition()
|
||||
|
||||
setCursorBufferPosition: (position) ->
|
||||
@moveCursors (cursor) -> cursor.setBufferPosition(position)
|
||||
|
||||
getCursorBufferPosition: ->
|
||||
@getLastCursor().getBufferPosition()
|
||||
|
||||
getSelectedScreenRange: ->
|
||||
@getLastSelection().getScreenRange()
|
||||
|
||||
getSelectedBufferRange: ->
|
||||
@getLastSelection().getBufferRange()
|
||||
|
||||
getSelectedBufferRanges: ->
|
||||
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
|
||||
|
||||
getSelectedText: ->
|
||||
@getLastSelection().getText()
|
||||
|
||||
getTextInBufferRange: (range) ->
|
||||
@buffer.getTextInRange(range)
|
||||
|
||||
moveCursorUp: ->
|
||||
@moveCursors (cursor) -> cursor.moveUp()
|
||||
|
||||
moveCursorDown: ->
|
||||
@moveCursors (cursor) -> cursor.moveDown()
|
||||
|
||||
moveCursorLeft: ->
|
||||
@moveCursors (cursor) -> cursor.moveLeft()
|
||||
|
||||
moveCursorRight: ->
|
||||
@moveCursors (cursor) -> cursor.moveRight()
|
||||
|
||||
moveCursorToTop: ->
|
||||
@moveCursors (cursor) -> cursor.moveToTop()
|
||||
|
||||
moveCursorToBottom: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBottom()
|
||||
|
||||
moveCursorToBeginningOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBeginningOfLine()
|
||||
|
||||
moveCursorToFirstCharacterOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()
|
||||
|
||||
moveCursorToEndOfLine: ->
|
||||
@moveCursors (cursor) -> cursor.moveToEndOfLine()
|
||||
|
||||
moveCursorToNextWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToNextWord()
|
||||
|
||||
moveCursorToBeginningOfWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToBeginningOfWord()
|
||||
|
||||
moveCursorToEndOfWord: ->
|
||||
@moveCursors (cursor) -> cursor.moveToEndOfWord()
|
||||
|
||||
moveCursors: (fn) ->
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
|
||||
selectToScreenPosition: (position) ->
|
||||
lastSelection = @getLastSelection()
|
||||
lastSelection.selectToScreenPosition(position)
|
||||
@mergeIntersectingSelections(reverse: lastSelection.isReversed())
|
||||
|
||||
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()
|
||||
|
||||
selectAll: ->
|
||||
@expandSelectionsForward (selection) => selection.selectAll()
|
||||
|
||||
selectToBottom: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToBottom()
|
||||
|
||||
selectToBeginningOfLine: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectToBeginningOfLine()
|
||||
|
||||
selectToEndOfLine: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToEndOfLine()
|
||||
|
||||
selectLine: ->
|
||||
@expandSelectionsForward (selection) => selection.selectLine()
|
||||
|
||||
expandLastSelectionOverLine: ->
|
||||
@getLastSelection().expandOverLine()
|
||||
|
||||
selectToBeginningOfWord: ->
|
||||
@expandSelectionsBackward (selection) => selection.selectToBeginningOfWord()
|
||||
|
||||
selectToEndOfWord: ->
|
||||
@expandSelectionsForward (selection) => selection.selectToEndOfWord()
|
||||
|
||||
selectWord: ->
|
||||
@expandSelectionsForward (selection) => selection.selectWord()
|
||||
|
||||
expandLastSelectionOverWord: ->
|
||||
@getLastSelection().expandOverWord()
|
||||
|
||||
mergeCursors: ->
|
||||
positions = []
|
||||
for cursor in new Array(@getCursors()...)
|
||||
position = cursor.getBufferPosition().toString()
|
||||
if position in positions
|
||||
cursor.destroy()
|
||||
else
|
||||
positions.push(position)
|
||||
|
||||
expandSelectionsForward: (fn) ->
|
||||
fn(selection) for selection in @getSelections()
|
||||
@mergeIntersectingSelections()
|
||||
|
||||
expandSelectionsBackward: (fn) ->
|
||||
fn(selection) for selection in @getSelections()
|
||||
@mergeIntersectingSelections(reverse: 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
|
||||
|
||||
inspect: ->
|
||||
JSON.stringify @serialize()
|
||||
|
||||
_.extend(EditSession.prototype, EventEmitter)
|
||||
869
src/app/editor.coffee
Normal file
869
src/app/editor.coffee
Normal file
@@ -0,0 +1,869 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
Buffer = require 'buffer'
|
||||
Gutter = require 'gutter'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
EditSession = require 'edit-session'
|
||||
CursorView = require 'cursor-view'
|
||||
SelectionView = require 'selection-view'
|
||||
Native = require 'native'
|
||||
fs = require 'fs'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Editor extends View
|
||||
@idCounter: 1
|
||||
|
||||
@content: (params) ->
|
||||
@div class: @classes(params), tabindex: -1, =>
|
||||
@input class: 'hidden-input', outlet: 'hiddenInput'
|
||||
@div class: 'flexbox', =>
|
||||
@subview 'gutter', new Gutter
|
||||
@div class: 'scroll-view', outlet: 'scrollView', =>
|
||||
@div class: 'lines', outlet: 'renderedLines', =>
|
||||
@div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
|
||||
@div outlet: 'verticalScrollbarContent'
|
||||
|
||||
@classes: ({mini} = {}) ->
|
||||
classes = ['editor']
|
||||
classes.push 'mini' if mini
|
||||
classes.join(' ')
|
||||
|
||||
vScrollMargin: 2
|
||||
hScrollMargin: 10
|
||||
lineHeight: null
|
||||
charWidth: null
|
||||
charHeight: null
|
||||
cursorViews: null
|
||||
selectionViews: null
|
||||
lineCache: null
|
||||
isFocused: false
|
||||
activeEditSession: null
|
||||
editSessions: null
|
||||
attached: false
|
||||
lineOverdraw: 100
|
||||
|
||||
@deserialize: (state, rootView) ->
|
||||
editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, rootView.project)
|
||||
editor = new Editor(editSession: editSessions[state.activeEditSessionIndex], mini: state.mini)
|
||||
editor.editSessions = editSessions
|
||||
editor.isFocused = state.isFocused
|
||||
editor
|
||||
|
||||
initialize: ({editSession, @mini} = {}) ->
|
||||
requireStylesheet 'editor.css'
|
||||
|
||||
@id = Editor.idCounter++
|
||||
@lineCache = []
|
||||
@bindKeys()
|
||||
@handleEvents()
|
||||
@cursorViews = []
|
||||
@selectionViews = []
|
||||
@editSessions = []
|
||||
|
||||
if editSession?
|
||||
@editSessions.push editSession
|
||||
@setActiveEditSessionIndex(0)
|
||||
else if @mini
|
||||
editSession = new EditSession
|
||||
buffer: new Buffer()
|
||||
softWrap: false
|
||||
tabText: " "
|
||||
autoIndent: false
|
||||
softTabs: true
|
||||
|
||||
@editSessions.push editSession
|
||||
@setActiveEditSessionIndex(0)
|
||||
else
|
||||
throw new Error("Editor initialization requires an editSession")
|
||||
|
||||
serialize: ->
|
||||
@saveActiveEditSession()
|
||||
|
||||
viewClass: "Editor"
|
||||
editSessions: @editSessions.map (session) -> session.serialize()
|
||||
activeEditSessionIndex: @getActiveEditSessionIndex()
|
||||
isFocused: @isFocused
|
||||
|
||||
copy: ->
|
||||
Editor.deserialize(@serialize(), @rootView())
|
||||
|
||||
bindKeys: ->
|
||||
editorBindings =
|
||||
'move-right': @moveCursorRight
|
||||
'move-left': @moveCursorLeft
|
||||
'move-down': @moveCursorDown
|
||||
'move-up': @moveCursorUp
|
||||
'move-to-next-word': @moveCursorToNextWord
|
||||
'move-to-previous-word': @moveCursorToPreviousWord
|
||||
'select-right': @selectRight
|
||||
'select-left': @selectLeft
|
||||
'select-up': @selectUp
|
||||
'select-down': @selectDown
|
||||
'select-word': @selectWord
|
||||
'newline': @insertNewline
|
||||
'indent': @indent
|
||||
'indent-selected-rows': @indentSelectedRows
|
||||
'outdent-selected-rows': @outdentSelectedRows
|
||||
'backspace': @backspace
|
||||
'backspace-to-beginning-of-word': @backspaceToBeginningOfWord
|
||||
'delete': @delete
|
||||
'delete-to-end-of-word': @deleteToEndOfWord
|
||||
'cut-to-end-of-line': @cutToEndOfLine
|
||||
'cut': @cutSelection
|
||||
'copy': @copySelection
|
||||
'paste': @paste
|
||||
'undo': @undo
|
||||
'redo': @redo
|
||||
'move-to-top': @moveCursorToTop
|
||||
'move-to-bottom': @moveCursorToBottom
|
||||
'move-to-beginning-of-line': @moveCursorToBeginningOfLine
|
||||
'move-to-end-of-line': @moveCursorToEndOfLine
|
||||
'move-to-first-character-of-line': @moveCursorToFirstCharacterOfLine
|
||||
'move-to-beginning-of-word': @moveCursorToBeginningOfWord
|
||||
'move-to-end-of-word': @moveCursorToEndOfWord
|
||||
'select-to-top': @selectToTop
|
||||
'select-to-bottom': @selectToBottom
|
||||
'select-to-end-of-line': @selectToEndOfLine
|
||||
'select-to-beginning-of-line': @selectToBeginningOfLine
|
||||
'select-to-end-of-word': @selectToEndOfWord
|
||||
'select-to-beginning-of-word': @selectToBeginningOfWord
|
||||
'select-all': @selectAll
|
||||
|
||||
if not @mini
|
||||
_.extend editorBindings,
|
||||
'save': @save
|
||||
'newline-below': @insertNewlineBelow
|
||||
'toggle-soft-wrap': @toggleSoftWrap
|
||||
'fold-all': @foldAll
|
||||
'unfold-all': @unfoldAll
|
||||
'fold-current-row': @foldCurrentRow
|
||||
'unfold-current-row': @unfoldCurrentRow
|
||||
'fold-selection': @foldSelection
|
||||
'split-left': @splitLeft
|
||||
'split-right': @splitRight
|
||||
'split-up': @splitUp
|
||||
'split-down': @splitDown
|
||||
'close': @close
|
||||
'show-next-buffer': @loadNextEditSession
|
||||
'show-previous-buffer': @loadPreviousEditSession
|
||||
'toggle-line-comments': @toggleLineCommentsInSelection
|
||||
|
||||
|
||||
for name, method of editorBindings
|
||||
do (name, method) =>
|
||||
@on name, => method.call(this); false
|
||||
|
||||
getCursor: (index) -> @activeEditSession.getCursor(index)
|
||||
getCursors: -> @activeEditSession.getCursors()
|
||||
getLastCursor: -> @activeEditSession.getLastCursor()
|
||||
addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition)
|
||||
addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition)
|
||||
moveCursorUp: -> @activeEditSession.moveCursorUp()
|
||||
moveCursorDown: -> @activeEditSession.moveCursorDown()
|
||||
moveCursorLeft: -> @activeEditSession.moveCursorLeft()
|
||||
moveCursorRight: -> @activeEditSession.moveCursorRight()
|
||||
moveCursorToNextWord: -> @activeEditSession.moveCursorToNextWord()
|
||||
moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord()
|
||||
moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord()
|
||||
moveCursorToTop: -> @activeEditSession.moveCursorToTop()
|
||||
moveCursorToBottom: -> @activeEditSession.moveCursorToBottom()
|
||||
moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine()
|
||||
moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine()
|
||||
moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine()
|
||||
setCursorScreenPosition: (position) -> @activeEditSession.setCursorScreenPosition(position)
|
||||
getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition()
|
||||
setCursorBufferPosition: (position) -> @activeEditSession.setCursorBufferPosition(position)
|
||||
getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition()
|
||||
|
||||
getSelection: (index) -> @activeEditSession.getSelection(index)
|
||||
getSelections: -> @activeEditSession.getSelections()
|
||||
getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition()
|
||||
getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer()
|
||||
getSelectedText: -> @activeEditSession.getSelectedText()
|
||||
getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges()
|
||||
getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange()
|
||||
setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options)
|
||||
setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options)
|
||||
addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options)
|
||||
selectRight: -> @activeEditSession.selectRight()
|
||||
selectLeft: -> @activeEditSession.selectLeft()
|
||||
selectUp: -> @activeEditSession.selectUp()
|
||||
selectDown: -> @activeEditSession.selectDown()
|
||||
selectToTop: -> @activeEditSession.selectToTop()
|
||||
selectToBottom: -> @activeEditSession.selectToBottom()
|
||||
selectAll: -> @activeEditSession.selectAll()
|
||||
selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine()
|
||||
selectToEndOfLine: -> @activeEditSession.selectToEndOfLine()
|
||||
selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord()
|
||||
selectToEndOfWord: -> @activeEditSession.selectToEndOfWord()
|
||||
selectWord: -> @activeEditSession.selectWord()
|
||||
selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position)
|
||||
clearSelections: -> @activeEditSession.clearSelections()
|
||||
|
||||
backspace: -> @activeEditSession.backspace()
|
||||
backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord()
|
||||
delete: -> @activeEditSession.delete()
|
||||
deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord()
|
||||
cutToEndOfLine: -> @activeEditSession.cutToEndOfLine()
|
||||
insertText: (text) -> @activeEditSession.insertText(text)
|
||||
insertNewline: -> @activeEditSession.insertNewline()
|
||||
insertNewlineBelow: -> @activeEditSession.insertNewlineBelow()
|
||||
indent: -> @activeEditSession.indent()
|
||||
indentSelectedRows: -> @activeEditSession.indentSelectedRows()
|
||||
outdentSelectedRows: -> @activeEditSession.outdentSelectedRows()
|
||||
cutSelection: -> @activeEditSession.cutSelectedText()
|
||||
copySelection: -> @activeEditSession.copySelectedText()
|
||||
paste: -> @activeEditSession.pasteText()
|
||||
undo: -> @activeEditSession.undo()
|
||||
redo: -> @activeEditSession.redo()
|
||||
createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow)
|
||||
foldCurrentRow: -> @activeEditSession.foldCurrentRow()
|
||||
unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow()
|
||||
foldAll: -> @activeEditSession.foldAll()
|
||||
unfoldAll: -> @activeEditSession.unfoldAll()
|
||||
foldSelection: -> @activeEditSession.foldSelection()
|
||||
destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId)
|
||||
destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow)
|
||||
isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow)
|
||||
|
||||
lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow)
|
||||
linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end)
|
||||
screenLineCount: -> @activeEditSession.screenLineCount()
|
||||
setSoftWrapColumn: (softWrapColumn) ->
|
||||
softWrapColumn ?= @calcSoftWrapColumn()
|
||||
@activeEditSession.setSoftWrapColumn(softWrapColumn) if softWrapColumn
|
||||
|
||||
maxScreenLineLength: -> @activeEditSession.maxScreenLineLength()
|
||||
getLastScreenRow: -> @activeEditSession.getLastScreenRow()
|
||||
clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options)
|
||||
screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options)
|
||||
bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options)
|
||||
screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range)
|
||||
bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range)
|
||||
bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow)
|
||||
stateForScreenRow: (row) -> @activeEditSession.stateForScreenRow(row)
|
||||
|
||||
setText: (text) -> @getBuffer().setText(text)
|
||||
getText: -> @getBuffer().getText()
|
||||
getPath: -> @getBuffer().getPath()
|
||||
getLastBufferRow: -> @getBuffer().getLastRow()
|
||||
getTextInRange: (range) -> @getBuffer().getTextInRange(range)
|
||||
getEofPosition: -> @getBuffer().getEofPosition()
|
||||
lineForBufferRow: (row) -> @getBuffer().lineForRow(row)
|
||||
lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row)
|
||||
rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row)
|
||||
scanInRange: (args...) -> @getBuffer().scanInRange(args...)
|
||||
backwardsScanInRange: (args...) -> @getBuffer().backwardsScanInRange(args...)
|
||||
|
||||
handleEvents: ->
|
||||
@on 'focus', =>
|
||||
@hiddenInput.focus()
|
||||
false
|
||||
|
||||
@hiddenInput.on 'focus', =>
|
||||
@rootView()?.editorFocused(this)
|
||||
@isFocused = true
|
||||
@addClass 'focused'
|
||||
|
||||
@hiddenInput.on 'focusout', =>
|
||||
@isFocused = false
|
||||
@removeClass 'focused'
|
||||
|
||||
@renderedLines.on 'mousedown', '.fold.line', (e) =>
|
||||
@destroyFold($(e.currentTarget).attr('fold-id'))
|
||||
false
|
||||
|
||||
@renderedLines.on 'mousedown', (e) =>
|
||||
clickCount = e.originalEvent.detail
|
||||
|
||||
screenPosition = @screenPositionFromMouseEvent(e)
|
||||
if clickCount == 1
|
||||
if e.metaKey
|
||||
@addCursorAtScreenPosition(screenPosition)
|
||||
else if e.shiftKey
|
||||
@selectToScreenPosition(screenPosition)
|
||||
else
|
||||
@setCursorScreenPosition(screenPosition)
|
||||
else if clickCount == 2
|
||||
if e.shiftKey
|
||||
@activeEditSession.expandLastSelectionOverWord()
|
||||
else
|
||||
@activeEditSession.selectWord()
|
||||
else if clickCount >= 3
|
||||
if e.shiftKey
|
||||
@activeEditSession.expandLastSelectionOverLine()
|
||||
else
|
||||
@activeEditSession.selectLine()
|
||||
|
||||
@selectOnMousemoveUntilMouseup()
|
||||
|
||||
@on "textInput", (e) =>
|
||||
@insertText(e.originalEvent.data)
|
||||
false
|
||||
|
||||
@scrollView.on 'mousewheel', (e) =>
|
||||
e = e.originalEvent
|
||||
if e.wheelDeltaY
|
||||
newEvent = document.createEvent("WheelEvent");
|
||||
newEvent.initWebKitWheelEvent(0, e.wheelDeltaY, e.view, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey)
|
||||
@verticalScrollbar.get(0).dispatchEvent(newEvent)
|
||||
false
|
||||
|
||||
@verticalScrollbar.on 'scroll', =>
|
||||
@scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false)
|
||||
|
||||
@scrollView.on 'scroll', =>
|
||||
if @scrollView.scrollLeft() == 0
|
||||
@gutter.removeClass('drop-shadow')
|
||||
else
|
||||
@gutter.addClass('drop-shadow')
|
||||
|
||||
selectOnMousemoveUntilMouseup: ->
|
||||
moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
|
||||
@on 'mousemove', moveHandler
|
||||
$(document).one 'mouseup', =>
|
||||
@off 'mousemove', moveHandler
|
||||
reverse = @activeEditSession.getLastSelection().isReversed()
|
||||
@activeEditSession.mergeIntersectingSelections({reverse})
|
||||
@syncCursorAnimations()
|
||||
|
||||
afterAttach: (onDom) ->
|
||||
return if @attached or not onDom
|
||||
@attached = true
|
||||
@clearRenderedLines()
|
||||
@subscribeToFontSize()
|
||||
@calculateDimensions()
|
||||
@hiddenInput.width(@charWidth)
|
||||
@setSoftWrapColumn() if @activeEditSession.getSoftWrap()
|
||||
$(window).on "resize.editor#{@id}", =>
|
||||
@updateRenderedLines()
|
||||
@focus() if @isFocused
|
||||
|
||||
@renderWhenAttached()
|
||||
|
||||
@trigger 'editor-open', [this]
|
||||
|
||||
edit: (editSession) ->
|
||||
index = @editSessions.indexOf(editSession)
|
||||
|
||||
if index == -1
|
||||
index = @editSessions.length
|
||||
@editSessions.push(editSession)
|
||||
|
||||
@setActiveEditSessionIndex(index)
|
||||
|
||||
getBuffer: -> @activeEditSession.buffer
|
||||
|
||||
destroyActiveEditSession: ->
|
||||
if @editSessions.length == 1
|
||||
@remove()
|
||||
else
|
||||
editSession = @activeEditSession
|
||||
@loadPreviousEditSession()
|
||||
_.remove(@editSessions, editSession)
|
||||
editSession.destroy()
|
||||
|
||||
loadNextEditSession: ->
|
||||
nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length
|
||||
@setActiveEditSessionIndex(nextIndex)
|
||||
|
||||
loadPreviousEditSession: ->
|
||||
previousIndex = @getActiveEditSessionIndex() - 1
|
||||
previousIndex = @editSessions.length - 1 if previousIndex < 0
|
||||
@setActiveEditSessionIndex(previousIndex)
|
||||
|
||||
getActiveEditSessionIndex: ->
|
||||
return index for session, index in @editSessions when session == @activeEditSession
|
||||
|
||||
setActiveEditSessionIndex: (index) ->
|
||||
throw new Error("Edit session not found") unless @editSessions[index]
|
||||
|
||||
if @activeEditSession
|
||||
@saveActiveEditSession()
|
||||
@activeEditSession.off()
|
||||
|
||||
@activeEditSession = @editSessions[index]
|
||||
|
||||
@activeEditSession.on "buffer-contents-change-on-disk", =>
|
||||
@showBufferConflictAlert(@activeEditSession)
|
||||
|
||||
@activeEditSession.on "buffer-path-change", =>
|
||||
@trigger 'editor-path-change'
|
||||
|
||||
@trigger 'editor-path-change'
|
||||
@renderWhenAttached()
|
||||
|
||||
if @attached and @activeEditSession.buffer.isInConflict()
|
||||
@showBufferConflictAlert(@activeEditSession)
|
||||
|
||||
showBufferConflictAlert: (editSession) ->
|
||||
message = editSession.getPath()
|
||||
detailedMessage = "Has changed on disk. Do you want to reload it?"
|
||||
Native.alert message, detailedMessage, [
|
||||
["Reload", => editSession.buffer.reload()]
|
||||
["Cancel", => ],
|
||||
]
|
||||
|
||||
activateEditSessionForPath: (path) ->
|
||||
for editSession, index in @editSessions
|
||||
if editSession.buffer.getPath() == path
|
||||
@setActiveEditSessionIndex(index)
|
||||
return @activeEditSession
|
||||
false
|
||||
|
||||
getOpenBufferPaths: ->
|
||||
editSession.buffer.getPath() for editSession in @editSessions when editSession.buffer.getPath()?
|
||||
|
||||
scrollTop: (scrollTop, options) ->
|
||||
return @cachedScrollTop or 0 unless scrollTop?
|
||||
|
||||
maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
|
||||
scrollTop = Math.floor(Math.min(maxScrollTop, Math.max(0, scrollTop)))
|
||||
|
||||
return if scrollTop == @cachedScrollTop
|
||||
@cachedScrollTop = scrollTop
|
||||
|
||||
@updateRenderedLines() if @attached
|
||||
|
||||
@renderedLines.css('top', -scrollTop)
|
||||
@gutter.lineNumbers.css('top', -scrollTop)
|
||||
if options?.adjustVerticalScrollbar ? true
|
||||
@verticalScrollbar.scrollTop(scrollTop)
|
||||
|
||||
scrollBottom: (scrollBottom) ->
|
||||
if scrollBottom?
|
||||
@scrollTop(scrollBottom - @scrollView.height())
|
||||
else
|
||||
@scrollTop() + @scrollView.height()
|
||||
|
||||
scrollToBottom: ->
|
||||
@scrollBottom(@scrollView.prop('scrollHeight'))
|
||||
|
||||
scrollTo: (pixelPosition) ->
|
||||
return unless @attached
|
||||
@scrollVertically(pixelPosition)
|
||||
@scrollHorizontally(pixelPosition)
|
||||
|
||||
scrollVertically: (pixelPosition) ->
|
||||
linesInView = @scrollView.height() / @lineHeight
|
||||
maxScrollMargin = Math.floor((linesInView - 1) / 2)
|
||||
scrollMargin = Math.min(@vScrollMargin, maxScrollMargin)
|
||||
margin = scrollMargin * @lineHeight
|
||||
desiredTop = pixelPosition.top - margin
|
||||
desiredBottom = pixelPosition.top + @lineHeight + margin
|
||||
|
||||
scrollViewHeight = @scrollView.height()
|
||||
if desiredBottom > @scrollTop() + scrollViewHeight
|
||||
@scrollTop(desiredBottom - scrollViewHeight)
|
||||
else if desiredTop < @scrollTop()
|
||||
@scrollTop(desiredTop)
|
||||
|
||||
scrollHorizontally: (pixelPosition) ->
|
||||
return if @activeEditSession.getSoftWrap()
|
||||
|
||||
charsInView = @scrollView.width() / @charWidth
|
||||
maxScrollMargin = Math.floor((charsInView - 1) / 2)
|
||||
scrollMargin = Math.min(@hScrollMargin, maxScrollMargin)
|
||||
margin = scrollMargin * @charWidth
|
||||
desiredRight = pixelPosition.left + @charWidth + margin
|
||||
desiredLeft = pixelPosition.left - margin
|
||||
|
||||
if desiredRight > @scrollView.scrollRight()
|
||||
@scrollView.scrollRight(desiredRight)
|
||||
else if desiredLeft < @scrollView.scrollLeft()
|
||||
@scrollView.scrollLeft(desiredLeft)
|
||||
|
||||
highlightFoldsContainingBufferRange: (bufferRange) ->
|
||||
screenLines = @linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow)
|
||||
for screenLine, i in screenLines
|
||||
if fold = screenLine.fold
|
||||
screenRow = @firstRenderedScreenRow + i
|
||||
element = @lineElementForScreenRow(screenRow)
|
||||
|
||||
if bufferRange.intersectsWith(fold.getBufferRange())
|
||||
element.addClass('selected')
|
||||
else
|
||||
element.removeClass('selected')
|
||||
|
||||
setScrollPositionFromActiveEditSession: ->
|
||||
@scrollTop(@activeEditSession.scrollTop ? 0)
|
||||
@scrollView.scrollLeft(@activeEditSession.scrollLeft ? 0)
|
||||
|
||||
saveActiveEditSession: ->
|
||||
@activeEditSession.setScrollTop(@scrollTop())
|
||||
@activeEditSession.setScrollLeft(@scrollView.scrollLeft())
|
||||
|
||||
toggleSoftWrap: ->
|
||||
@setSoftWrap(not @activeEditSession.getSoftWrap())
|
||||
|
||||
calcSoftWrapColumn: ->
|
||||
if @activeEditSession.getSoftWrap()
|
||||
Math.floor(@scrollView.width() / @charWidth)
|
||||
else
|
||||
Infinity
|
||||
|
||||
setSoftWrap: (softWrap, softWrapColumn=undefined) ->
|
||||
@activeEditSession.setSoftWrap(softWrap)
|
||||
@setSoftWrapColumn(softWrapColumn) if @attached
|
||||
if @activeEditSession.getSoftWrap()
|
||||
@addClass 'soft-wrap'
|
||||
@_setSoftWrapColumn = => @setSoftWrapColumn()
|
||||
$(window).on 'resize', @_setSoftWrapColumn
|
||||
else
|
||||
@removeClass 'soft-wrap'
|
||||
$(window).off 'resize', @_setSoftWrapColumn
|
||||
|
||||
save: ->
|
||||
if not @getPath()
|
||||
path = Native.saveDialog()
|
||||
return false if not path
|
||||
@getBuffer().saveAs(path)
|
||||
else
|
||||
@getBuffer().save()
|
||||
true
|
||||
|
||||
subscribeToFontSize: ->
|
||||
return unless rootView = @rootView()
|
||||
@setFontSize(rootView.getFontSize())
|
||||
rootView.on "font-size-change.editor#{@id}", => @setFontSize(rootView.getFontSize())
|
||||
|
||||
setFontSize: (fontSize) ->
|
||||
if fontSize?
|
||||
@css('font-size', fontSize + 'px')
|
||||
@calculateDimensions()
|
||||
@updateCursorViews()
|
||||
@updateRenderedLines()
|
||||
|
||||
newSplitEditor: ->
|
||||
new Editor { editSession: @activeEditSession.copy() }
|
||||
|
||||
splitLeft: ->
|
||||
@pane()?.splitLeft(@newSplitEditor()).wrappedView
|
||||
|
||||
splitRight: ->
|
||||
@pane()?.splitRight(@newSplitEditor()).wrappedView
|
||||
|
||||
splitUp: ->
|
||||
@pane()?.splitUp(@newSplitEditor()).wrappedView
|
||||
|
||||
splitDown: ->
|
||||
@pane()?.splitDown(@newSplitEditor()).wrappedView
|
||||
|
||||
pane: ->
|
||||
@parent('.pane').view()
|
||||
|
||||
rootView: ->
|
||||
@parents('#root-view').view()
|
||||
|
||||
close: ->
|
||||
return if @mini
|
||||
if @getBuffer().isModified()
|
||||
filename = if @getPath() then fs.base(@getPath()) else "untitled buffer"
|
||||
message = "'#{filename}' has changes, do you want to save them?"
|
||||
detailedMessage = "Your changes will be lost if you don't save them"
|
||||
buttons = [
|
||||
["Save", => @save() and @destroyActiveEditSession()]
|
||||
["Cancel", =>]
|
||||
["Don't save", => @destroyActiveEditSession()]
|
||||
]
|
||||
|
||||
Native.alert message, detailedMessage, buttons
|
||||
else
|
||||
@destroyActiveEditSession()
|
||||
|
||||
remove: (selector, keepData) ->
|
||||
return super if keepData
|
||||
|
||||
@trigger 'before-remove'
|
||||
|
||||
@destroyEditSessions()
|
||||
|
||||
$(window).off ".editor#{@id}"
|
||||
rootView = @rootView()
|
||||
rootView?.off ".editor#{@id}"
|
||||
if @pane() then @pane().remove() else super
|
||||
rootView?.focus()
|
||||
|
||||
getEditSessions: ->
|
||||
new Array(@editSessions...)
|
||||
|
||||
destroyEditSessions: ->
|
||||
for session in @getEditSessions()
|
||||
session.destroy()
|
||||
|
||||
renderWhenAttached: ->
|
||||
return unless @attached
|
||||
|
||||
@removeAllCursorAndSelectionViews()
|
||||
@addCursorView(cursor) for cursor in @activeEditSession.getCursors()
|
||||
@addSelectionView(selection) for selection in @activeEditSession.getSelections()
|
||||
@activeEditSession.on 'add-cursor', (cursor) => @addCursorView(cursor)
|
||||
@activeEditSession.on 'add-selection', (selection) => @addSelectionView(selection)
|
||||
|
||||
@prepareForScrolling()
|
||||
@setScrollPositionFromActiveEditSession()
|
||||
|
||||
@renderLines()
|
||||
@activeEditSession.on 'screen-lines-change', (e) => @handleDisplayBufferChange(e)
|
||||
|
||||
getCursorView: (index) ->
|
||||
index ?= @cursorViews.length - 1
|
||||
@cursorViews[index]
|
||||
|
||||
getCursorViews: ->
|
||||
new Array(@cursorViews...)
|
||||
|
||||
addCursorView: (cursor) ->
|
||||
cursorView = new CursorView(cursor, this)
|
||||
@cursorViews.push(cursorView)
|
||||
@renderedLines.append(cursorView)
|
||||
cursorView
|
||||
|
||||
removeCursorView: (cursorView) ->
|
||||
_.remove(@cursorViews, cursorView)
|
||||
|
||||
updateCursorViews: ->
|
||||
for cursorView in @getCursorViews()
|
||||
cursorView.updateAppearance()
|
||||
|
||||
syncCursorAnimations: ->
|
||||
for cursorView in @getCursorViews()
|
||||
do (cursorView) -> cursorView.resetCursorAnimation()
|
||||
|
||||
getSelectionView: (index) ->
|
||||
index ?= @selectionViews.length - 1
|
||||
@selectionViews[index]
|
||||
|
||||
getSelectionViews: ->
|
||||
new Array(@selectionViews...)
|
||||
|
||||
addSelectionView: (selection) ->
|
||||
selectionView = new SelectionView({editor: this, selection})
|
||||
@selectionViews.push(selectionView)
|
||||
@renderedLines.append(selectionView)
|
||||
selectionView
|
||||
|
||||
removeSelectionView: (selectionView) ->
|
||||
_.remove(@selectionViews, selectionView)
|
||||
|
||||
removeAllCursorAndSelectionViews: ->
|
||||
cursorView.remove() for cursorView in @getCursorViews()
|
||||
selectionView.remove() for selectionView in @getSelectionViews()
|
||||
|
||||
calculateDimensions: ->
|
||||
fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
|
||||
@renderedLines.append(fragment)
|
||||
@charWidth = fragment.width()
|
||||
@charHeight = fragment.find('span').height()
|
||||
@lineHeight = fragment.outerHeight()
|
||||
@height(@lineHeight) if @mini
|
||||
fragment.remove()
|
||||
|
||||
@gutter.calculateDimensions()
|
||||
|
||||
prepareForScrolling: ->
|
||||
@adjustHeightOfRenderedLines()
|
||||
@adjustWidthOfRenderedLines()
|
||||
|
||||
adjustHeightOfRenderedLines: ->
|
||||
heightOfRenderedLines = @lineHeight * @screenLineCount()
|
||||
@verticalScrollbarContent.height(heightOfRenderedLines)
|
||||
@renderedLines.css('padding-bottom', heightOfRenderedLines)
|
||||
|
||||
adjustWidthOfRenderedLines: ->
|
||||
width = @charWidth * @maxScreenLineLength()
|
||||
if width > @scrollView.width()
|
||||
@renderedLines.width(width)
|
||||
else
|
||||
@renderedLines.css('width', '')
|
||||
|
||||
handleScrollHeightChange: ->
|
||||
scrollHeight = @lineHeight * @screenLineCount()
|
||||
@verticalScrollbarContent.height(scrollHeight)
|
||||
@scrollBottom(scrollHeight) if @scrollBottom() > scrollHeight
|
||||
|
||||
renderLines: ->
|
||||
@clearRenderedLines()
|
||||
@updateRenderedLines()
|
||||
|
||||
clearRenderedLines: ->
|
||||
@lineCache = []
|
||||
@renderedLines.find('.line').remove()
|
||||
|
||||
@firstRenderedScreenRow = -1
|
||||
@lastRenderedScreenRow = -1
|
||||
|
||||
updateRenderedLines: ->
|
||||
firstVisibleScreenRow = @getFirstVisibleScreenRow()
|
||||
lastVisibleScreenRow = @getLastVisibleScreenRow()
|
||||
renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw)
|
||||
renderTo = Math.min(@getLastScreenRow(), lastVisibleScreenRow + @lineOverdraw)
|
||||
|
||||
if firstVisibleScreenRow < @firstRenderedScreenRow
|
||||
@removeLineElements(Math.max(@firstRenderedScreenRow, renderTo + 1), @lastRenderedScreenRow)
|
||||
@lastRenderedScreenRow = renderTo
|
||||
newLines = @buildLineElements(renderFrom, Math.min(@firstRenderedScreenRow - 1, renderTo))
|
||||
@insertLineElements(renderFrom, newLines)
|
||||
@firstRenderedScreenRow = renderFrom
|
||||
renderedLines = true
|
||||
|
||||
if lastVisibleScreenRow > @lastRenderedScreenRow
|
||||
if 0 <= @firstRenderedScreenRow < renderFrom
|
||||
@removeLineElements(@firstRenderedScreenRow, Math.min(@lastRenderedScreenRow, renderFrom - 1))
|
||||
@firstRenderedScreenRow = renderFrom
|
||||
startRowOfNewLines = Math.max(@lastRenderedScreenRow + 1, renderFrom)
|
||||
newLines = @buildLineElements(startRowOfNewLines, renderTo)
|
||||
@insertLineElements(startRowOfNewLines, newLines)
|
||||
@lastRenderedScreenRow = renderTo
|
||||
renderedLines = true
|
||||
|
||||
if renderedLines
|
||||
@gutter.renderLineNumbers(renderFrom, renderTo)
|
||||
@updatePaddingOfRenderedLines()
|
||||
|
||||
updatePaddingOfRenderedLines: ->
|
||||
paddingTop = @firstRenderedScreenRow * @lineHeight
|
||||
@renderedLines.css('padding-top', paddingTop)
|
||||
@gutter.lineNumbers.css('padding-top', paddingTop)
|
||||
|
||||
paddingBottom = (@getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight
|
||||
@renderedLines.css('padding-bottom', paddingBottom)
|
||||
@gutter.lineNumbers.css('padding-bottom', paddingBottom)
|
||||
|
||||
getFirstVisibleScreenRow: ->
|
||||
Math.floor(@scrollTop() / @lineHeight)
|
||||
|
||||
getLastVisibleScreenRow: ->
|
||||
Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
|
||||
|
||||
handleDisplayBufferChange: (e) ->
|
||||
oldScreenRange = e.oldRange
|
||||
newScreenRange = e.newRange
|
||||
|
||||
if @attached
|
||||
@handleScrollHeightChange() unless newScreenRange.coversSameRows(oldScreenRange)
|
||||
@adjustWidthOfRenderedLines()
|
||||
|
||||
return if oldScreenRange.start.row > @lastRenderedScreenRow
|
||||
|
||||
maxEndRow = Math.max(@getLastVisibleScreenRow() + @lineOverdraw, @lastRenderedScreenRow)
|
||||
@gutter.renderLineNumbers(@firstRenderedScreenRow, maxEndRow) if e.lineNumbersChanged
|
||||
|
||||
newScreenRange = newScreenRange.copy()
|
||||
oldScreenRange = oldScreenRange.copy()
|
||||
endOfShortestRange = Math.min(oldScreenRange.end.row, newScreenRange.end.row)
|
||||
|
||||
delta = @firstRenderedScreenRow - endOfShortestRange
|
||||
if delta > 0
|
||||
newScreenRange.start.row += delta
|
||||
newScreenRange.end.row += delta
|
||||
oldScreenRange.start.row += delta
|
||||
oldScreenRange.end.row += delta
|
||||
|
||||
newScreenRange.start.row = Math.max(newScreenRange.start.row, @firstRenderedScreenRow)
|
||||
oldScreenRange.end.row = Math.min(oldScreenRange.end.row, @lastRenderedScreenRow)
|
||||
oldScreenRange.start.row = Math.max(oldScreenRange.start.row, @firstRenderedScreenRow)
|
||||
newScreenRange.end.row = Math.min(newScreenRange.end.row, maxEndRow)
|
||||
|
||||
lineElements = @buildLineElements(newScreenRange.start.row, newScreenRange.end.row)
|
||||
@replaceLineElements(oldScreenRange.start.row, oldScreenRange.end.row, lineElements)
|
||||
|
||||
rowDelta = newScreenRange.end.row - oldScreenRange.end.row
|
||||
@lastRenderedScreenRow += rowDelta
|
||||
@updateRenderedLines() if rowDelta < 0
|
||||
|
||||
if @lastRenderedScreenRow > maxEndRow
|
||||
@removeLineElements(maxEndRow + 1, @lastRenderedScreenRow)
|
||||
@lastRenderedScreenRow = maxEndRow
|
||||
@updatePaddingOfRenderedLines()
|
||||
|
||||
buildLineElements: (startRow, endRow) ->
|
||||
charWidth = @charWidth
|
||||
charHeight = @charHeight
|
||||
lines = @activeEditSession.linesForScreenRows(startRow, endRow)
|
||||
activeEditSession = @activeEditSession
|
||||
|
||||
$$ ->
|
||||
for line in lines
|
||||
if fold = line.fold
|
||||
lineAttributes = { class: 'fold line', 'fold-id': fold.id }
|
||||
if activeEditSession.selectionIntersectsBufferRange(fold.getBufferRange())
|
||||
lineAttributes.class += ' selected'
|
||||
else
|
||||
lineAttributes = { class: 'line' }
|
||||
@div lineAttributes, =>
|
||||
if line.text == ''
|
||||
@raw ' ' if line.text == ''
|
||||
else
|
||||
for token in line.tokens
|
||||
@span { class: token.getCssClassString() }, token.value
|
||||
|
||||
insertLineElements: (row, lineElements) ->
|
||||
@spliceLineElements(row, 0, lineElements)
|
||||
|
||||
replaceLineElements: (startRow, endRow, lineElements) ->
|
||||
@spliceLineElements(startRow, endRow - startRow + 1, lineElements)
|
||||
|
||||
removeLineElements: (startRow, endRow) ->
|
||||
@spliceLineElements(startRow, endRow - startRow + 1)
|
||||
|
||||
spliceLineElements: (startScreenRow, rowCount, lineElements) ->
|
||||
throw new Error("Splicing at a negative start row: #{startScreenRow}") if startScreenRow < 0
|
||||
|
||||
if startScreenRow < @firstRenderedScreenRow
|
||||
startRow = 0
|
||||
else
|
||||
startRow = startScreenRow - @firstRenderedScreenRow
|
||||
|
||||
endRow = startRow + rowCount
|
||||
|
||||
elementToInsertBefore = @lineCache[startRow]
|
||||
elementsToReplace = @lineCache[startRow...endRow]
|
||||
@lineCache[startRow...endRow] = lineElements?.toArray() or []
|
||||
|
||||
lines = @renderedLines[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)
|
||||
|
||||
lineElementForScreenRow: (screenRow) ->
|
||||
element = @lineCache[screenRow - @firstRenderedScreenRow]
|
||||
$(element)
|
||||
|
||||
logScreenLines: (start, end) ->
|
||||
@activeEditSession.logScreenLines(start, end)
|
||||
|
||||
toggleLineCommentsInSelection: ->
|
||||
@activeEditSession.toggleLineCommentsInSelection()
|
||||
|
||||
logRenderedLines: ->
|
||||
@renderedLines.find('.line').each (n) ->
|
||||
console.log n, $(this).text()
|
||||
|
||||
pixelPositionForScreenPosition: (position) ->
|
||||
position = Point.fromObject(position)
|
||||
{ top: position.row * @lineHeight, left: position.column * @charWidth }
|
||||
|
||||
pixelOffsetForScreenPosition: (position) ->
|
||||
{top, left} = @pixelPositionForScreenPosition(position)
|
||||
offset = @renderedLines.offset()
|
||||
{top: top + offset.top, left: left + offset.left}
|
||||
|
||||
screenPositionFromPixelPosition: ({top, left}) ->
|
||||
screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth))
|
||||
|
||||
screenPositionFromMouseEvent: (e) ->
|
||||
{ pageX, pageY } = e
|
||||
@screenPositionFromPixelPosition
|
||||
top: pageY - @scrollView.offset().top + @scrollTop()
|
||||
left: pageX - @scrollView.offset().left + @scrollView.scrollLeft()
|
||||
62
src/app/event-emitter.coffee
Normal file
62
src/app/event-emitter.coffee
Normal file
@@ -0,0 +1,62 @@
|
||||
_ = 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)
|
||||
|
||||
@afterSubscribe?()
|
||||
|
||||
trigger: (eventName, args...) ->
|
||||
[eventName, namespace] = eventName.split('.')
|
||||
|
||||
if namespace
|
||||
@eventHandlersByNamespace?[namespace]?[eventName]?.forEach (handler) -> handler(args...)
|
||||
else
|
||||
@eventHandlersByEventName?[eventName]?.forEach (handler) -> handler(args...)
|
||||
|
||||
off: (eventName='', handler) ->
|
||||
[eventName, namespace] = eventName.split('.')
|
||||
eventName = undefined if eventName == ''
|
||||
|
||||
subscriptionCountBefore = @subscriptionCount()
|
||||
|
||||
if !eventName? and !namespace?
|
||||
@eventHandlersByEventName = {}
|
||||
@eventHandlersByNamespace = {}
|
||||
else if namespace
|
||||
if eventName
|
||||
handlers = @eventHandlersByNamespace?[namespace]?[eventName] ? []
|
||||
for handler in new Array(handlers...)
|
||||
_.remove(handlers, handler)
|
||||
@off eventName, handler
|
||||
return
|
||||
else
|
||||
for eventName, handlers of @eventHandlersByNamespace?[namespace] ? {}
|
||||
for handler in new Array(handlers...)
|
||||
_.remove(handlers, handler)
|
||||
@off eventName, handler
|
||||
return
|
||||
else
|
||||
if handler
|
||||
_.remove(@eventHandlersByEventName[eventName], handler)
|
||||
else
|
||||
delete @eventHandlersByEventName?[eventName]
|
||||
|
||||
@afterUnsubscribe?() if @subscriptionCount() < subscriptionCountBefore
|
||||
|
||||
subscriptionCount: ->
|
||||
count = 0
|
||||
for name, handlers of @eventHandlersByEventName
|
||||
count += handlers.length
|
||||
count
|
||||
|
||||
50
src/app/file.coffee
Normal file
50
src/app/file.coffee
Normal file
@@ -0,0 +1,50 @@
|
||||
EventEmitter = require 'event-emitter'
|
||||
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class File
|
||||
path: null
|
||||
md5: null
|
||||
|
||||
constructor: (@path) ->
|
||||
throw "Creating file with path that is not a file: #{@path}" unless fs.isFile(@path)
|
||||
@updateMd5()
|
||||
|
||||
setPath: (@path) ->
|
||||
|
||||
getPath: ->
|
||||
@path
|
||||
|
||||
getBaseName: ->
|
||||
fs.base(@path)
|
||||
|
||||
updateMd5: ->
|
||||
@md5 = fs.md5ForPath(@path)
|
||||
|
||||
afterSubscribe: ->
|
||||
@subscribeToNativeChangeEvents() if @subscriptionCount() == 1
|
||||
|
||||
afterUnsubscribe: ->
|
||||
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
|
||||
|
||||
subscribeToNativeChangeEvents: ->
|
||||
@watchId = $native.watchPath @path, (eventType, path) =>
|
||||
if eventType is "remove"
|
||||
@trigger "remove"
|
||||
@off()
|
||||
else if eventType is "move"
|
||||
@setPath(path)
|
||||
@trigger "move"
|
||||
else if eventType is "contents-change"
|
||||
newMd5 = fs.md5ForPath(@getPath())
|
||||
return if newMd5 == @md5
|
||||
|
||||
@md5 = newMd5
|
||||
@trigger 'contents-change'
|
||||
|
||||
unsubscribeFromNativeChangeEvents: ->
|
||||
$native.unwatchPath(@path, @watchId)
|
||||
|
||||
_.extend File.prototype, EventEmitter
|
||||
74
src/app/fold.coffee
Normal file
74
src/app/fold.coffee
Normal file
@@ -0,0 +1,74 @@
|
||||
Range = require 'range'
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class Fold
|
||||
@idCounter: 1
|
||||
|
||||
displayBuffer: null
|
||||
startRow: null
|
||||
endRow: null
|
||||
|
||||
constructor: (@displayBuffer, @startRow, @endRow) ->
|
||||
@id = @constructor.idCounter++
|
||||
|
||||
destroy: ->
|
||||
@displayBuffer.destroyFold(this)
|
||||
|
||||
inspect: ->
|
||||
"Fold(#{@startRow}, #{@endRow})"
|
||||
|
||||
getBufferRange: ({includeNewline}={}) ->
|
||||
if includeNewline
|
||||
end = [@endRow + 1, 0]
|
||||
else
|
||||
end = [@endRow, Infinity]
|
||||
|
||||
new Range([@startRow, 0], end)
|
||||
|
||||
getBufferDelta: ->
|
||||
new Point(@endRow - @startRow + 1, 0)
|
||||
|
||||
handleBufferChange: (event) ->
|
||||
oldStartRow = @startRow
|
||||
|
||||
if @isContainedByRange(event.oldRange)
|
||||
@displayBuffer.unregisterFold(@startRow, this)
|
||||
return
|
||||
|
||||
@updateStartRow(event)
|
||||
@updateEndRow(event)
|
||||
|
||||
if @startRow != oldStartRow
|
||||
@displayBuffer.unregisterFold(oldStartRow, this)
|
||||
@displayBuffer.registerFold(this)
|
||||
|
||||
isContainedByRange: (range) ->
|
||||
range.start.row <= @startRow and @endRow <= range.end.row
|
||||
|
||||
isContainedByFold: (fold) ->
|
||||
@isContainedByRange(fold.getBufferRange())
|
||||
|
||||
updateStartRow: (event) ->
|
||||
{ newRange, oldRange } = event
|
||||
|
||||
if oldRange.end.row < @startRow
|
||||
delta = newRange.end.row - oldRange.end.row
|
||||
else if newRange.end.row < @startRow
|
||||
delta = newRange.end.row - @startRow
|
||||
else
|
||||
delta = 0
|
||||
|
||||
@startRow += delta
|
||||
|
||||
updateEndRow: (event) ->
|
||||
{ newRange, oldRange } = event
|
||||
|
||||
if oldRange.end.row <= @endRow
|
||||
delta = newRange.end.row - oldRange.end.row
|
||||
else if newRange.end.row <= @endRow
|
||||
delta = newRange.end.row - @endRow
|
||||
else
|
||||
delta = 0
|
||||
|
||||
@endRow += delta
|
||||
27
src/app/gutter.coffee
Normal file
27
src/app/gutter.coffee
Normal file
@@ -0,0 +1,27 @@
|
||||
{View, $$$} = require 'space-pen'
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Gutter extends View
|
||||
@content: ->
|
||||
@div class: 'gutter', =>
|
||||
@div outlet: 'lineNumbers', class: 'line-numbers'
|
||||
|
||||
editor: ->
|
||||
editor = @parentView
|
||||
|
||||
renderLineNumbers: (startScreenRow, endScreenRow) ->
|
||||
lastScreenRow = -1
|
||||
rows = @editor().bufferRowsForScreenRows(startScreenRow, endScreenRow)
|
||||
|
||||
@lineNumbers[0].innerHTML = $$$ ->
|
||||
for row in rows
|
||||
@div {class: 'line-number'}, if row == lastScreenRow then '•' else row + 1
|
||||
lastScreenRow = row
|
||||
|
||||
@calculateDimensions()
|
||||
|
||||
calculateDimensions: ->
|
||||
@lineNumbers.width(@editor().getLastBufferRow().toString().length * @editor().charWidth)
|
||||
122
src/app/keymap.coffee
Normal file
122
src/app/keymap.coffee
Normal file
@@ -0,0 +1,122 @@
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
|
||||
BindingSet = require 'binding-set'
|
||||
Specificity = require 'specificity'
|
||||
|
||||
module.exports =
|
||||
class Keymap
|
||||
bindingSets: null
|
||||
queuedKeystrokes: 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.configFilePath)
|
||||
$(document).on 'open', =>
|
||||
path = $native.openDialog()
|
||||
atom.open(path) if path
|
||||
|
||||
bindKeys: (selector, bindings) ->
|
||||
index = @bindingSets.length
|
||||
@bindingSets.unshift(new BindingSet(selector, bindings, index))
|
||||
|
||||
bindingsForElement: (element) ->
|
||||
keystrokeMap = {}
|
||||
currentNode = $(element)
|
||||
|
||||
while currentNode.length
|
||||
bindingSets = @bindingSetsForNode(currentNode)
|
||||
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
|
||||
currentNode = currentNode.parent()
|
||||
|
||||
keystrokeMap
|
||||
|
||||
handleKeyEvent: (event) ->
|
||||
event.keystrokes = @multiKeystrokeStringForEvent(event)
|
||||
isMultiKeystroke = @queuedKeystrokes?
|
||||
@queuedKeystrokes = null
|
||||
currentNode = $(event.target)
|
||||
while currentNode.length
|
||||
candidateBindingSets = @bindingSetsForNode(currentNode)
|
||||
for bindingSet in candidateBindingSets
|
||||
command = bindingSet.commandForEvent(event)
|
||||
if command
|
||||
continue if @triggerCommandEvent(event, command)
|
||||
return false
|
||||
else if command == false
|
||||
return false
|
||||
|
||||
if bindingSet.matchesKeystrokePrefix(event)
|
||||
@queuedKeystrokes = event.keystrokes
|
||||
return false
|
||||
currentNode = currentNode.parent()
|
||||
|
||||
!isMultiKeystroke
|
||||
|
||||
bindingSetsForNode: (node) ->
|
||||
bindingSets = @bindingSets.filter (set) -> node.is(set.selector)
|
||||
bindingSets.sort (a, b) ->
|
||||
if b.specificity == a.specificity
|
||||
b.index - a.index
|
||||
else
|
||||
b.specificity - a.specificity
|
||||
|
||||
triggerCommandEvent: (keyEvent, commandName) ->
|
||||
commandEvent = $.Event(commandName)
|
||||
commandEvent.keyEvent = keyEvent
|
||||
aborted = false
|
||||
commandEvent.abortKeyBinding = ->
|
||||
@stopImmediatePropagation()
|
||||
aborted = true
|
||||
$(keyEvent.target).trigger(commandEvent)
|
||||
aborted
|
||||
|
||||
multiKeystrokeStringForEvent: (event) ->
|
||||
currentKeystroke = @keystrokeStringForEvent(event)
|
||||
if @queuedKeystrokes
|
||||
@queuedKeystrokes + ' ' + currentKeystroke
|
||||
else
|
||||
currentKeystroke
|
||||
|
||||
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'
|
||||
7
src/app/keymaps/atom.coffee
Normal file
7
src/app/keymaps/atom.coffee
Normal file
@@ -0,0 +1,7 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'meta-w': 'close'
|
||||
'alt-meta-i': 'toggle-dev-tools'
|
||||
right: 'move-right'
|
||||
left: 'move-left'
|
||||
down: 'move-down'
|
||||
up: 'move-up'
|
||||
38
src/app/keymaps/editor.coffee
Normal file
38
src/app/keymaps/editor.coffee
Normal file
@@ -0,0 +1,38 @@
|
||||
window.keymap.bindKeys '.editor',
|
||||
'meta-s': 'save'
|
||||
'shift-right': 'select-right'
|
||||
'shift-left': 'select-left'
|
||||
'shift-up': 'select-up'
|
||||
'shift-down': 'select-down'
|
||||
'meta-a': 'select-all'
|
||||
'enter': 'newline'
|
||||
'meta-enter': 'newline-below'
|
||||
'tab': 'indent'
|
||||
'backspace': 'backspace'
|
||||
'shift-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'
|
||||
'ctrl-[': 'fold-current-row'
|
||||
'ctrl-]': 'unfold-current-row'
|
||||
'ctrl-{': 'fold-all'
|
||||
'ctrl-}': 'unfold-all'
|
||||
'alt-meta-ctrl-f': 'fold-selection'
|
||||
'alt-meta-left': 'split-left'
|
||||
'alt-meta-right': 'split-right'
|
||||
'alt-meta-up': 'split-up'
|
||||
'alt-meta-down': 'split-down'
|
||||
'shift-tab': 'outdent-selected-rows'
|
||||
'meta-[': 'outdent-selected-rows'
|
||||
'meta-]': 'indent-selected-rows'
|
||||
'meta-{': 'show-previous-buffer'
|
||||
'meta-}': 'show-next-buffer'
|
||||
'meta-+': 'increase-font-size'
|
||||
'meta--': 'decrease-font-size'
|
||||
'meta-/': 'toggle-line-comments'
|
||||
'ctrl-w w': 'focus-next-pane'
|
||||
'ctrl-W': 'select-word'
|
||||
22
src/app/keymaps/emacs.coffee
Normal file
22
src/app/keymaps/emacs.coffee
Normal file
@@ -0,0 +1,22 @@
|
||||
window.keymap.bindKeys '*',
|
||||
'ctrl-f': 'move-right'
|
||||
'ctrl-b': 'move-left'
|
||||
'ctrl-p': 'move-up'
|
||||
'ctrl-n': 'move-down'
|
||||
|
||||
window.keymap.bindKeys '.editor',
|
||||
'ctrl-F': 'select-right'
|
||||
'ctrl-B': 'select-left'
|
||||
'ctrl-P': 'select-up'
|
||||
'ctrl-N': 'select-down'
|
||||
'alt-f': 'move-to-end-of-word'
|
||||
'alt-F': 'select-to-end-of-word'
|
||||
'alt-b': 'move-to-beginning-of-word'
|
||||
'alt-B': 'select-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'
|
||||
'ctrl-k': 'cut-to-end-of-line'
|
||||
3
src/app/keystroke-pattern.pegjs
Normal file
3
src/app/keystroke-pattern.pegjs
Normal file
@@ -0,0 +1,3 @@
|
||||
keystrokePattern = key:key additionalKeys:additionalKey* { return [key].concat(additionalKeys); }
|
||||
additionalKey = '-' key:key { return key; }
|
||||
key = '-' / chars:[^-]+ { return chars.join('') }
|
||||
127
src/app/language-mode.coffee
Normal file
127
src/app/language-mode.coffee
Normal file
@@ -0,0 +1,127 @@
|
||||
Range = require 'range'
|
||||
TextMateBundle = require 'text-mate-bundle'
|
||||
_ = require 'underscore'
|
||||
require 'underscore-extensions'
|
||||
|
||||
module.exports =
|
||||
class LanguageMode
|
||||
pairedCharacters:
|
||||
'(': ')'
|
||||
'[': ']'
|
||||
'{': '}'
|
||||
'"': '"'
|
||||
"'": "'"
|
||||
|
||||
constructor: (@editSession) ->
|
||||
@buffer = @editSession.buffer
|
||||
@grammar = TextMateBundle.grammarForFileName(@buffer.getBaseName())
|
||||
|
||||
_.adviseBefore @editSession, 'insertText', (text) =>
|
||||
return true if @editSession.hasMultipleCursors()
|
||||
|
||||
cursorBufferPosition = @editSession.getCursorBufferPosition()
|
||||
nextCharacter = @editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0, 1])])
|
||||
|
||||
if @isCloseBracket(text) and text == nextCharacter
|
||||
@editSession.moveCursorRight()
|
||||
false
|
||||
else if /^\s*$/.test(nextCharacter) and pairedCharacter = @pairedCharacters[text]
|
||||
@editSession.insertText text + pairedCharacter
|
||||
@editSession.moveCursorLeft()
|
||||
false
|
||||
|
||||
isOpenBracket: (string) ->
|
||||
@pairedCharacters[string]?
|
||||
|
||||
isCloseBracket: (string) ->
|
||||
@getInvertedPairedCharacters()[string]?
|
||||
|
||||
getInvertedPairedCharacters: ->
|
||||
return @invertedPairedCharacters if @invertedPairedCharacters
|
||||
|
||||
@invertedPairedCharacters = {}
|
||||
for open, close of @pairedCharacters
|
||||
@invertedPairedCharacters[close] = open
|
||||
@invertedPairedCharacters
|
||||
|
||||
toggleLineCommentsInRange: (range) ->
|
||||
range = Range.fromObject(range)
|
||||
scopes = @tokenizedBuffer.scopesForPosition(range.start)
|
||||
commentString = TextMateBundle.lineCommentStringForScope(scopes[0])
|
||||
commentRegex = new OnigRegExp("^\s*" + _.escapeRegExp(commentString))
|
||||
|
||||
shouldUncomment = commentRegex.test(@editSession.lineForBufferRow(range.start.row))
|
||||
|
||||
for row in [range.start.row..range.end.row]
|
||||
line = @editSession.lineForBufferRow(row)
|
||||
if shouldUncomment
|
||||
match = commentRegex.search(line)
|
||||
@editSession.buffer.change([[row, 0], [row, match[0].length]], "")
|
||||
else
|
||||
@editSession.buffer.insert([row, 0], commentString)
|
||||
|
||||
doesBufferRowStartFold: (bufferRow) ->
|
||||
return false if @editSession.isBufferRowBlank(bufferRow)
|
||||
nextNonEmptyRow = @editSession.nextNonBlankBufferRow(bufferRow)
|
||||
return false unless nextNonEmptyRow?
|
||||
@editSession.indentationForBufferRow(nextNonEmptyRow) > @editSession.indentationForBufferRow(bufferRow)
|
||||
|
||||
rowRangeForFoldAtBufferRow: (bufferRow) ->
|
||||
return null unless @doesBufferRowStartFold(bufferRow)
|
||||
|
||||
startIndentation = @editSession.indentationForBufferRow(bufferRow)
|
||||
for row in [(bufferRow + 1)..@editSession.getLastBufferRow()]
|
||||
continue if @editSession.isBufferRowBlank(row)
|
||||
indentation = @editSession.indentationForBufferRow(row)
|
||||
if indentation <= startIndentation
|
||||
includeRowInFold = indentation == startIndentation and @grammar.foldEndRegex.search(@editSession.lineForBufferRow(row))
|
||||
foldEndRow = row if includeRowInFold
|
||||
break
|
||||
|
||||
foldEndRow = row
|
||||
|
||||
[bufferRow, foldEndRow]
|
||||
|
||||
autoIndentBufferRows: (startRow, endRow) ->
|
||||
@autoIndentBufferRow(row) for row in [startRow..endRow]
|
||||
|
||||
autoIndentBufferRow: (bufferRow) ->
|
||||
@autoIncreaseIndentForBufferRow(bufferRow)
|
||||
@autoDecreaseIndentForBufferRow(bufferRow)
|
||||
|
||||
autoIncreaseIndentForBufferRow: (bufferRow) ->
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
return unless precedingRow?
|
||||
|
||||
precedingLine = @editSession.lineForBufferRow(precedingRow)
|
||||
scopes = @tokenizedBuffer.scopesForPosition([precedingRow, Infinity])
|
||||
increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0])
|
||||
return unless increaseIndentPattern
|
||||
|
||||
currentIndentation = @buffer.indentationForRow(bufferRow)
|
||||
desiredIndentation = @buffer.indentationForRow(precedingRow)
|
||||
desiredIndentation += @editSession.tabText.length if increaseIndentPattern.test(precedingLine)
|
||||
if desiredIndentation > currentIndentation
|
||||
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
|
||||
|
||||
autoDecreaseIndentForBufferRow: (bufferRow) ->
|
||||
scopes = @tokenizedBuffer.scopesForPosition([bufferRow, 0])
|
||||
increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0])
|
||||
decreaseIndentPattern = TextMateBundle.outdentRegexForScope(scopes[0])
|
||||
return unless increaseIndentPattern and decreaseIndentPattern
|
||||
|
||||
line = @buffer.lineForRow(bufferRow)
|
||||
return unless decreaseIndentPattern.test(line)
|
||||
|
||||
currentIndentation = @buffer.indentationForRow(bufferRow)
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
precedingLine = @buffer.lineForRow(precedingRow)
|
||||
|
||||
desiredIndentation = @buffer.indentationForRow(precedingRow)
|
||||
desiredIndentation -= @editSession.tabText.length unless increaseIndentPattern.test(precedingLine)
|
||||
if desiredIndentation < currentIndentation
|
||||
@buffer.setIndentationForRow(bufferRow, desiredIndentation)
|
||||
|
||||
getLineTokens: (line, stack) ->
|
||||
{tokens, stack} = @grammar.getLineTokens(line, stack)
|
||||
|
||||
186
src/app/line-map.coffee
Normal file
186
src/app/line-map.coffee
Normal file
@@ -0,0 +1,186 @@
|
||||
_ = 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
|
||||
|
||||
maxScreenLineLength: ->
|
||||
maxLength = 0
|
||||
@traverseByDelta 'screenDelta', [0, 0], [@lastScreenRow(), 0], ({lineFragment}) ->
|
||||
length = lineFragment.text.length
|
||||
maxLength = length if length > maxLength
|
||||
maxLength
|
||||
|
||||
screenPositionForBufferPosition: (bufferPosition, options) ->
|
||||
@translatePosition('bufferDelta', 'screenDelta', bufferPosition, options)
|
||||
|
||||
bufferPositionForScreenPosition: (screenPosition, options) ->
|
||||
@translatePosition('screenDelta', 'bufferDelta', screenPosition, options)
|
||||
|
||||
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
|
||||
traversedAllFragments = traversalResult.traversedAllFragments
|
||||
sourceDelta = traversalResult[sourceDeltaType]
|
||||
targetDelta = traversalResult[targetDeltaType]
|
||||
|
||||
return targetDelta unless lastLineFragment
|
||||
maxSourceColumn = sourceDelta.column + lastLineFragment.textLength()
|
||||
maxTargetColumn = targetDelta.column + lastLineFragment.textLength()
|
||||
|
||||
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 and not traversedAllFragments
|
||||
targetDelta.row++
|
||||
targetDelta.column = 0
|
||||
else
|
||||
additionalColumns = sourcePosition.column - sourceDelta.column
|
||||
additionalColumns = lastLineFragment.translateColumn(sourceDeltaType, targetDeltaType, additionalColumns, { skipAtomicTokens })
|
||||
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
|
||||
startPosition = Point.fromObject(startPosition)
|
||||
endPosition = Point.fromObject(endPosition)
|
||||
|
||||
for lineFragment, index 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)
|
||||
|
||||
lastLineFragment = lineFragment
|
||||
traversedAllFragments = (index == @lineFragments.length - 1)
|
||||
{ screenDelta, bufferDelta, lastLineFragment, traversedAllFragments }
|
||||
|
||||
logLines: (start=0, end=@screenLineCount() - 1)->
|
||||
for row in [start..end]
|
||||
line = @lineForScreenRow(row).text
|
||||
console.log row, line, line.length
|
||||
|
||||
32
src/app/pane-column.coffee
Normal file
32
src/app/pane-column.coffee
Normal file
@@ -0,0 +1,32 @@
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
PaneGrid = require 'pane-grid'
|
||||
|
||||
module.exports =
|
||||
class PaneColumn extends PaneGrid
|
||||
@content: ->
|
||||
@div class: 'column'
|
||||
|
||||
className: ->
|
||||
"PaneColumn"
|
||||
|
||||
adjustDimensions: ->
|
||||
totalUnits = @verticalGridUnits()
|
||||
unitsSoFar = 0
|
||||
for child in @children()
|
||||
child = $(child).view()
|
||||
childUnits = child.verticalGridUnits()
|
||||
child.css
|
||||
width: '100%'
|
||||
height: "#{childUnits / totalUnits * 100}%"
|
||||
top: "#{unitsSoFar / totalUnits * 100}%"
|
||||
left: 0
|
||||
|
||||
child.adjustDimensions()
|
||||
unitsSoFar += childUnits
|
||||
|
||||
horizontalGridUnits: ->
|
||||
Math.max(@horizontalChildUnits()...)
|
||||
|
||||
verticalGridUnits: ->
|
||||
_.sum(@verticalChildUnits())
|
||||
24
src/app/pane-grid.coffee
Normal file
24
src/app/pane-grid.coffee
Normal file
@@ -0,0 +1,24 @@
|
||||
$ = require 'jquery'
|
||||
{View} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class PaneGrid extends View
|
||||
@deserialize: ({children}, rootView) ->
|
||||
childViews = children.map (child) -> rootView.deserializeView(child)
|
||||
new this(childViews)
|
||||
|
||||
initialize: (children=[]) ->
|
||||
@append(children...)
|
||||
|
||||
serialize: ->
|
||||
viewClass: @className()
|
||||
children: @childViewStates()
|
||||
|
||||
childViewStates: ->
|
||||
$(child).view().serialize() for child in @children()
|
||||
|
||||
horizontalChildUnits: ->
|
||||
$(child).view().horizontalGridUnits() for child in @children()
|
||||
|
||||
verticalChildUnits: ->
|
||||
$(child).view().verticalGridUnits() for child in @children()
|
||||
32
src/app/pane-row.coffee
Normal file
32
src/app/pane-row.coffee
Normal file
@@ -0,0 +1,32 @@
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
PaneGrid = require 'pane-grid'
|
||||
|
||||
module.exports =
|
||||
class PaneRow extends PaneGrid
|
||||
@content: ->
|
||||
@div class: 'row'
|
||||
|
||||
className: ->
|
||||
"PaneRow"
|
||||
|
||||
adjustDimensions: ->
|
||||
totalUnits = @horizontalGridUnits()
|
||||
unitsSoFar = 0
|
||||
for child in @children()
|
||||
child = $(child).view()
|
||||
childUnits = child.horizontalGridUnits()
|
||||
child.css
|
||||
width: "#{childUnits / totalUnits * 100}%"
|
||||
height: '100%'
|
||||
top: 0
|
||||
left: "#{unitsSoFar / totalUnits * 100}%"
|
||||
|
||||
child.adjustDimensions()
|
||||
unitsSoFar += childUnits
|
||||
|
||||
horizontalGridUnits: ->
|
||||
_.sum(@horizontalChildUnits())
|
||||
|
||||
verticalGridUnits: ->
|
||||
Math.max(@verticalChildUnits()...)
|
||||
67
src/app/pane.coffee
Normal file
67
src/app/pane.coffee
Normal file
@@ -0,0 +1,67 @@
|
||||
{View} = require 'space-pen'
|
||||
PaneRow = require 'pane-row'
|
||||
PaneColumn = require 'pane-column'
|
||||
|
||||
module.exports =
|
||||
class Pane extends View
|
||||
@content: (wrappedView) ->
|
||||
@div class: 'pane', =>
|
||||
@subview 'wrappedView', wrappedView
|
||||
|
||||
@deserialize: ({wrappedView}, rootView) ->
|
||||
new Pane(rootView.deserializeView(wrappedView))
|
||||
|
||||
serialize: ->
|
||||
viewClass: "Pane"
|
||||
wrappedView: @wrappedView.serialize()
|
||||
|
||||
adjustDimensions: -> # do nothing
|
||||
|
||||
horizontalGridUnits: ->
|
||||
1
|
||||
|
||||
verticalGridUnits: ->
|
||||
1
|
||||
|
||||
splitUp: (view) ->
|
||||
@split(view, 'column', 'before')
|
||||
|
||||
splitDown: (view) ->
|
||||
@split(view, 'column', 'after')
|
||||
|
||||
splitLeft: (view) ->
|
||||
@split(view, 'row', 'before')
|
||||
|
||||
splitRight: (view) ->
|
||||
@split(view, 'row', 'after')
|
||||
|
||||
split: (view, axis, side) ->
|
||||
unless @parent().hasClass(axis)
|
||||
@buildPaneAxis(axis)
|
||||
.insertBefore(this)
|
||||
.append(@detach())
|
||||
|
||||
pane = new Pane(view)
|
||||
this[side](pane)
|
||||
@rootView().adjustPaneDimensions()
|
||||
view.focus?()
|
||||
pane
|
||||
|
||||
remove: (selector, keepData) ->
|
||||
return super if keepData
|
||||
# find parent elements before removing from dom
|
||||
parentAxis = @parent('.row, .column')
|
||||
rootView = @rootView()
|
||||
super
|
||||
if parentAxis.children().length == 1
|
||||
sibling = parentAxis.children().detach()
|
||||
parentAxis.replaceWith(sibling)
|
||||
rootView.adjustPaneDimensions()
|
||||
|
||||
buildPaneAxis: (axis) ->
|
||||
switch axis
|
||||
when 'row' then new PaneRow
|
||||
when 'column' then new PaneColumn
|
||||
|
||||
rootView: ->
|
||||
@parents('#root-view').view()
|
||||
87
src/app/point.coffee
Normal file
87
src/app/point.coffee
Normal file
@@ -0,0 +1,87 @@
|
||||
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) ->
|
||||
other = Point.fromObject(other)
|
||||
row = @row + other.row
|
||||
if other.row == 0
|
||||
column = @column + other.column
|
||||
else
|
||||
column = other.column
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
subtract: (other) ->
|
||||
other = Point.fromObject(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) ->
|
||||
return false unless other
|
||||
other = Point.fromObject(other)
|
||||
@row == other.row and @column == other.column
|
||||
|
||||
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}"
|
||||
|
||||
toArray: ->
|
||||
[@row, @column]
|
||||
|
||||
serialize: ->
|
||||
@toArray()
|
||||
179
src/app/project.coffee
Normal file
179
src/app/project.coffee
Normal file
@@ -0,0 +1,179 @@
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
Range = require 'range'
|
||||
Buffer = require 'buffer'
|
||||
EditSession = require 'edit-session'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Directory = require 'directory'
|
||||
ChildProcess = require 'child-process'
|
||||
|
||||
module.exports =
|
||||
class Project
|
||||
tabText: ' '
|
||||
autoIndent: true
|
||||
softTabs: true
|
||||
softWrap: false
|
||||
rootDirectory: null
|
||||
editSessions: null
|
||||
ignoredPathRegexes: null
|
||||
|
||||
constructor: (path) ->
|
||||
@setPath(path)
|
||||
@editSessions = []
|
||||
@buffers = []
|
||||
@ignoredPathRegexes = [
|
||||
/\.DS_Store$/
|
||||
/(^|\/)\.git(\/|$)/
|
||||
]
|
||||
|
||||
destroy: ->
|
||||
editSession.destroy() for editSession in @getEditSessions()
|
||||
|
||||
getPath: ->
|
||||
@rootDirectory?.path
|
||||
|
||||
setPath: (path) ->
|
||||
@rootDirectory?.off()
|
||||
|
||||
if path?
|
||||
directory = if fs.isDirectory(path) then path else fs.directory(path)
|
||||
@rootDirectory = new Directory(directory)
|
||||
else
|
||||
@rootDirectory = null
|
||||
|
||||
@trigger "path-change"
|
||||
|
||||
getRootDirectory: ->
|
||||
@rootDirectory
|
||||
|
||||
getFilePaths: ->
|
||||
deferred = $.Deferred()
|
||||
|
||||
filePaths = []
|
||||
fs.traverseTree @getPath(), (path, prune) =>
|
||||
if @ignorePath(path)
|
||||
prune()
|
||||
else if fs.isFile(path)
|
||||
filePaths.push @relativize(path)
|
||||
|
||||
deferred.resolve filePaths
|
||||
deferred
|
||||
|
||||
ignorePath: (path) ->
|
||||
_.find @ignoredPathRegexes, (regex) -> path.match(regex)
|
||||
|
||||
ignorePathRegex: ->
|
||||
@ignoredPathRegexes.map((regex) -> "(#{regex.source})").join("|")
|
||||
|
||||
resolve: (filePath) ->
|
||||
filePath = fs.join(@getPath(), filePath) unless filePath[0] == '/'
|
||||
fs.absolute filePath
|
||||
|
||||
relativize: (fullPath) ->
|
||||
fullPath.replace(@getPath(), "").replace(/^\//, '')
|
||||
|
||||
getTabText: -> @tabText
|
||||
setTabText: (@tabText) ->
|
||||
|
||||
getAutoIndent: -> @autoIndent
|
||||
setAutoIndent: (@autoIndent) ->
|
||||
|
||||
getSoftTabs: -> @softTabs
|
||||
setSoftTabs: (@softTabs) ->
|
||||
|
||||
getSoftWrap: -> @softWrap
|
||||
setSoftWrap: (@softWrap) ->
|
||||
|
||||
buildEditSessionForPath: (filePath, editSessionOptions={}) ->
|
||||
@buildEditSession(@bufferForPath(filePath), editSessionOptions)
|
||||
|
||||
buildEditSession: (buffer, editSessionOptions) ->
|
||||
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
|
||||
options.project = this
|
||||
options.buffer = buffer
|
||||
editSession = new EditSession(options)
|
||||
@editSessions.push editSession
|
||||
@trigger 'new-edit-session', editSession
|
||||
editSession
|
||||
|
||||
defaultEditSessionOptions: ->
|
||||
tabText: @getTabText()
|
||||
autoIndent: @getAutoIndent()
|
||||
softTabs: @getSoftTabs()
|
||||
softWrap: @getSoftWrap()
|
||||
|
||||
getEditSessions: ->
|
||||
new Array(@editSessions...)
|
||||
|
||||
removeEditSession: (editSession) ->
|
||||
_.remove(@editSessions, editSession)
|
||||
|
||||
getBuffers: ->
|
||||
buffers = []
|
||||
for editSession in @editSessions when not _.include(buffers, editSession.buffer)
|
||||
buffers.push editSession.buffer
|
||||
|
||||
buffers
|
||||
|
||||
bufferForPath: (filePath) ->
|
||||
if filePath?
|
||||
filePath = @resolve(filePath)
|
||||
buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath
|
||||
buffer or @buildBuffer(filePath)
|
||||
else
|
||||
@buildBuffer()
|
||||
|
||||
buildBuffer: (filePath) ->
|
||||
buffer = new Buffer(filePath, this)
|
||||
@buffers.push buffer
|
||||
@trigger 'new-buffer', buffer
|
||||
buffer
|
||||
|
||||
removeBuffer: (buffer) ->
|
||||
_.remove(@buffers, buffer)
|
||||
|
||||
scan: (regex, iterator) ->
|
||||
regex = new RegExp(regex.source, 'g')
|
||||
command = "#{require.resolve('ag')} --ackmate \"#{regex.source}\" \"#{@getPath()}\""
|
||||
bufferedData = ""
|
||||
|
||||
state = 'readingPath'
|
||||
path = null
|
||||
|
||||
readPath = (line) ->
|
||||
if /^[0-9,; ]+:/.test(line)
|
||||
state = 'readingLines'
|
||||
else if /^:/.test line
|
||||
path = line.substr(1)
|
||||
else
|
||||
path += ('\n' + line)
|
||||
|
||||
readLine = (line) ->
|
||||
if line.length == 0
|
||||
state = 'readingPath'
|
||||
path = null
|
||||
else
|
||||
colonIndex = line.indexOf(':')
|
||||
matchInfo = line.substring(0, colonIndex)
|
||||
lineText = line.substring(colonIndex + 1)
|
||||
readMatches(matchInfo, lineText)
|
||||
|
||||
readMatches = (matchInfo, lineText) ->
|
||||
[lineNumber, matchPositionsText] = matchInfo.match(/(\d+);(.+)/)[1..]
|
||||
row = parseInt(lineNumber) - 1
|
||||
matchPositions = matchPositionsText.split(',').map (positionText) -> positionText.split(' ').map (pos) -> parseInt(pos)
|
||||
|
||||
for [column, length] in matchPositions
|
||||
range = new Range([row, column], [row, column + length])
|
||||
match = lineText.substr(column, length)
|
||||
iterator({path, range, match})
|
||||
|
||||
ChildProcess.exec command , bufferLines: true, stdout: (data) ->
|
||||
lines = data.split('\n')
|
||||
lines.pop() # the last segment is a spurios '' because data always ends in \n due to bufferLines: true
|
||||
for line in lines
|
||||
readPath(line) if state is 'readingPath'
|
||||
readLine(line) if state is 'readingLines'
|
||||
|
||||
_.extend Project.prototype, EventEmitter
|
||||
79
src/app/range.coffee
Normal file
79
src/app/range.coffee
Normal file
@@ -0,0 +1,79 @@
|
||||
Point = require 'point'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Range
|
||||
@fromObject: (object) ->
|
||||
if _.isArray(object)
|
||||
new Range(object...)
|
||||
else if object instanceof Range
|
||||
object
|
||||
else
|
||||
new Range(object.start, object.end)
|
||||
|
||||
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: ->
|
||||
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()}]"
|
||||
|
||||
add: (point) ->
|
||||
new Range(@start.add(point), @end.add(point))
|
||||
|
||||
intersectsWith: (otherRange) ->
|
||||
if @start.isLessThanOrEqual(otherRange.start)
|
||||
@end.isGreaterThanOrEqual(otherRange.start)
|
||||
else
|
||||
otherRange.intersectsWith(this)
|
||||
|
||||
containsPoint: (point, { exclusive } = {}) ->
|
||||
point = Point.fromObject(point)
|
||||
if exclusive
|
||||
point.isGreaterThan(@start) and point.isLessThan(@end)
|
||||
else
|
||||
point.isGreaterThanOrEqual(@start) and point.isLessThanOrEqual(@end)
|
||||
|
||||
containsRow: (row) ->
|
||||
@start.row <= row <= @end.row
|
||||
|
||||
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)
|
||||
|
||||
getRowCount: ->
|
||||
@end.row - @start.row + 1
|
||||
213
src/app/root-view.coffee
Normal file
213
src/app/root-view.coffee
Normal file
@@ -0,0 +1,213 @@
|
||||
$ = require 'jquery'
|
||||
{$$} = require 'space-pen'
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
|
||||
{View} = require 'space-pen'
|
||||
Buffer = require 'buffer'
|
||||
Editor = require 'editor'
|
||||
Project = require 'project'
|
||||
VimMode = require 'vim-mode'
|
||||
Pane = require 'pane'
|
||||
PaneColumn = require 'pane-column'
|
||||
PaneRow = require 'pane-row'
|
||||
StatusBar = require 'status-bar'
|
||||
TextMateTheme = require 'text-mate-theme'
|
||||
|
||||
module.exports =
|
||||
class RootView extends View
|
||||
@content: ->
|
||||
@div id: 'root-view', tabindex: -1, =>
|
||||
@div id: 'horizontal', outlet: 'horizontal', =>
|
||||
@div id: 'vertical', outlet: 'vertical', =>
|
||||
@div id: 'panes', outlet: 'panes'
|
||||
|
||||
@deserialize: ({ projectPath, panesViewState, extensionStates }) ->
|
||||
rootView = new RootView(projectPath, extensionStates: extensionStates, suppressOpen: true)
|
||||
rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState
|
||||
rootView
|
||||
|
||||
extensions: null
|
||||
extensionStates: null
|
||||
fontSize: 20
|
||||
|
||||
initialize: (pathToOpen, { @extensionStates, suppressOpen } = {}) ->
|
||||
window.rootView = this
|
||||
TextMateTheme.activate('IR_Black')
|
||||
|
||||
@extensionStates ?= {}
|
||||
@extensions = {}
|
||||
@project = new Project(pathToOpen)
|
||||
@handleEvents()
|
||||
@setTitle()
|
||||
@loadUserConfiguration()
|
||||
@open(pathToOpen) if fs.isFile(pathToOpen) unless suppressOpen
|
||||
|
||||
serialize: ->
|
||||
projectPath: @project?.getPath()
|
||||
panesViewState: @panes.children().view()?.serialize()
|
||||
extensionStates: @serializeExtensions()
|
||||
|
||||
handleEvents: ->
|
||||
@on 'toggle-dev-tools', => window.toggleDevTools()
|
||||
@on 'focus', (e) =>
|
||||
if @getActiveEditor()
|
||||
@getActiveEditor().focus()
|
||||
false
|
||||
else
|
||||
@setTitle(@project?.getPath())
|
||||
|
||||
@on 'active-editor-path-change', (e, path) =>
|
||||
@project.setPath(path) unless @project.getRootDirectory()
|
||||
@setTitle(path)
|
||||
|
||||
@on 'increase-font-size', => @setFontSize(@getFontSize() + 1)
|
||||
@on 'decrease-font-size', => @setFontSize(@getFontSize() - 1)
|
||||
@on 'focus-next-pane', => @focusNextPane()
|
||||
|
||||
afterAttach: (onDom) ->
|
||||
@focus() if onDom
|
||||
|
||||
serializeExtensions: ->
|
||||
extensionStates = {}
|
||||
for name, extension of @extensions
|
||||
try
|
||||
extensionStates[name] = extension.serialize?()
|
||||
catch e
|
||||
console?.error("Exception serializing '#{name}' extension\n", e.stack)
|
||||
extensionStates
|
||||
|
||||
deserializeView: (viewState) ->
|
||||
switch viewState.viewClass
|
||||
when 'Pane' then Pane.deserialize(viewState, this)
|
||||
when 'PaneRow' then PaneRow.deserialize(viewState, this)
|
||||
when 'PaneColumn' then PaneColumn.deserialize(viewState, this)
|
||||
when 'Editor' then Editor.deserialize(viewState, this)
|
||||
|
||||
activateExtension: (extension) ->
|
||||
throw new Error("Trying to activate an extension with no name") unless extension.name?
|
||||
@extensions[extension.name] = extension
|
||||
extension.activate(this, @extensionStates[extension.name])
|
||||
|
||||
deactivateExtension: (extension) ->
|
||||
extension.deactivate?()
|
||||
delete @extensions[extension.name]
|
||||
|
||||
deactivate: ->
|
||||
atom.rootViewStates[$windowNumber] = JSON.stringify(@serialize())
|
||||
@deactivateExtension(extension) for name, extension of @extensions
|
||||
@remove()
|
||||
|
||||
open: (path, options = {}) ->
|
||||
changeFocus = options.changeFocus ? true
|
||||
allowActiveEditorChange = options.allowActiveEditorChange ? false
|
||||
|
||||
unless editSession = @openInExistingEditor(path, allowActiveEditorChange)
|
||||
editSession = @project.buildEditSessionForPath(path)
|
||||
editor = new Editor({editSession})
|
||||
pane = new Pane(editor)
|
||||
@panes.append(pane)
|
||||
if changeFocus
|
||||
editor.focus()
|
||||
else
|
||||
@makeEditorActive(editor)
|
||||
|
||||
editSession
|
||||
|
||||
openInExistingEditor: (path, allowActiveEditorChange) ->
|
||||
if activeEditor = @getActiveEditor()
|
||||
path = @project.resolve(path) if path
|
||||
|
||||
if editSession = activeEditor.activateEditSessionForPath(path)
|
||||
return editSession
|
||||
|
||||
if allowActiveEditorChange
|
||||
for editor in @getEditors()
|
||||
if editSession = editor.activateEditSessionForPath(path)
|
||||
editor.focus()
|
||||
return editSession
|
||||
|
||||
editSession = @project.buildEditSessionForPath(path)
|
||||
activeEditor.edit(editSession)
|
||||
editSession
|
||||
|
||||
editorFocused: (editor) ->
|
||||
@makeEditorActive(editor) if @panes.containsElement(editor)
|
||||
|
||||
makeEditorActive: (editor) ->
|
||||
previousActiveEditor = @panes.find('.editor.active').view()
|
||||
previousActiveEditor?.removeClass('active').off('.root-view')
|
||||
editor.addClass('active')
|
||||
|
||||
if not editor.mini
|
||||
editor.on 'editor-path-change.root-view', =>
|
||||
@trigger 'active-editor-path-change', editor.getPath()
|
||||
if not previousActiveEditor or editor.getPath() != previousActiveEditor.getPath()
|
||||
@trigger 'active-editor-path-change', editor.getPath()
|
||||
|
||||
activeKeybindings: ->
|
||||
keymap.bindingsForElement(document.activeElement)
|
||||
|
||||
setTitle: (title='untitled') ->
|
||||
document.title = title
|
||||
|
||||
getEditors: ->
|
||||
@panes.find('.pane > .editor').map(-> $(this).view()).toArray()
|
||||
|
||||
getModifiedBuffers: ->
|
||||
modifiedBuffers = []
|
||||
for editor in @getEditors()
|
||||
for session in editor.editSessions
|
||||
modifiedBuffers.push session.buffer if session.buffer.isModified()
|
||||
|
||||
modifiedBuffers
|
||||
|
||||
getOpenBufferPaths: ->
|
||||
_.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths()))
|
||||
|
||||
getActiveEditor: ->
|
||||
if (editor = @panes.find('.editor.active')).length
|
||||
editor.view()
|
||||
else
|
||||
@panes.find('.editor:first').view()
|
||||
|
||||
getActiveEditSession: ->
|
||||
@getActiveEditor()?.activeEditSession
|
||||
|
||||
focusNextPane: ->
|
||||
panes = @panes.find('.pane')
|
||||
currentIndex = panes.toArray().indexOf(@getFocusedPane()[0])
|
||||
nextIndex = (currentIndex + 1) % panes.length
|
||||
panes.eq(nextIndex).view().wrappedView.focus()
|
||||
|
||||
getFocusedPane: ->
|
||||
@panes.find('.pane:has(:focus)')
|
||||
|
||||
setRootPane: (pane) ->
|
||||
@panes.empty()
|
||||
@panes.append(pane)
|
||||
@adjustPaneDimensions()
|
||||
|
||||
adjustPaneDimensions: ->
|
||||
rootPane = @panes.children().first().view()
|
||||
rootPane?.css(width: '100%', height: '100%', top: 0, left: 0)
|
||||
rootPane?.adjustDimensions()
|
||||
|
||||
remove: ->
|
||||
editor.remove() for editor in @getEditors()
|
||||
@project.destroy()
|
||||
super
|
||||
|
||||
setFontSize: (newFontSize) ->
|
||||
newFontSize = Math.max(1, newFontSize)
|
||||
[oldFontSize, @fontSize] = [@fontSize, newFontSize]
|
||||
@trigger 'font-size-change' if oldFontSize != newFontSize
|
||||
|
||||
getFontSize: -> @fontSize
|
||||
|
||||
loadUserConfiguration: ->
|
||||
try
|
||||
require atom.configFilePath if fs.exists(atom.configFilePath)
|
||||
catch error
|
||||
console.error "Failed to load `#{atom.configFilePath}`", error.message, error
|
||||
|
||||
92
src/app/screen-line.coffee
Normal file
92
src/app/screen-line.coffee
Normal file
@@ -0,0 +1,92 @@
|
||||
_ = require 'underscore'
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class ScreenLine
|
||||
stack: null
|
||||
text: null
|
||||
tokens: null
|
||||
screenDelta: null
|
||||
bufferDelta: null
|
||||
foldable: null
|
||||
|
||||
constructor: (@tokens, @text, screenDelta, bufferDelta, extraFields) ->
|
||||
@screenDelta = Point.fromObject(screenDelta)
|
||||
@bufferDelta = Point.fromObject(bufferDelta)
|
||||
_.extend(this, extraFields)
|
||||
|
||||
copy: ->
|
||||
new ScreenLine(@tokens, @text, @screenDelta, @bufferDelta, { @stack, @foldable })
|
||||
|
||||
splitAt: (column) ->
|
||||
return [new ScreenLine([], '', [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] = rightTokens[0].splitAt(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 ScreenLine(leftTokens, leftText, leftScreenDelta, leftBufferDelta, {@stack, @foldable})
|
||||
rightFragment = new ScreenLine(rightTokens, rightText, rightScreenDelta, rightBufferDelta, {@stack})
|
||||
[leftFragment, rightFragment]
|
||||
|
||||
tokenAtBufferColumn: (bufferColumn) ->
|
||||
delta = 0
|
||||
for token in @tokens
|
||||
delta += token.bufferDelta
|
||||
return token if delta >= bufferColumn
|
||||
token
|
||||
|
||||
concat: (other) ->
|
||||
tokens = @tokens.concat(other.tokens)
|
||||
text = @text + other.text
|
||||
screenDelta = @screenDelta.add(other.screenDelta)
|
||||
bufferDelta = @bufferDelta.add(other.bufferDelta)
|
||||
new ScreenLine(tokens, text, screenDelta, bufferDelta, {stack: other.stack})
|
||||
|
||||
translateColumn: (sourceDeltaType, targetDeltaType, sourceColumn, options={}) ->
|
||||
{ skipAtomicTokens } = options
|
||||
sourceColumn = Math.min(sourceColumn, @textLength())
|
||||
|
||||
currentSourceColumn = 0
|
||||
currentTargetColumn = 0
|
||||
for token in @tokens
|
||||
tokenStartTargetColumn = currentTargetColumn
|
||||
tokenStartSourceColumn = currentSourceColumn
|
||||
tokenEndSourceColumn = currentSourceColumn + token[sourceDeltaType]
|
||||
tokenEndTargetColumn = currentTargetColumn + token[targetDeltaType]
|
||||
break if tokenEndSourceColumn > sourceColumn
|
||||
currentSourceColumn = tokenEndSourceColumn
|
||||
currentTargetColumn = tokenEndTargetColumn
|
||||
|
||||
if token?.isAtomic
|
||||
if skipAtomicTokens and sourceColumn > tokenStartSourceColumn
|
||||
tokenEndTargetColumn
|
||||
else
|
||||
tokenStartTargetColumn
|
||||
else
|
||||
remainingColumns = sourceColumn - currentSourceColumn
|
||||
currentTargetColumn + remainingColumns
|
||||
|
||||
textLength: ->
|
||||
if @fold
|
||||
textLength = 0
|
||||
else
|
||||
textLength = @text.length
|
||||
|
||||
isSoftWrapped: ->
|
||||
@screenDelta.row == 1 and @bufferDelta.row == 0
|
||||
|
||||
isEqual: (other) ->
|
||||
_.isEqual(@tokens, other.tokens) and @screenDelta.isEqual(other.screenDelta) and @bufferDelta.isEqual(other.bufferDelta)
|
||||
61
src/app/selection-view.coffee
Normal file
61
src/app/selection-view.coffee
Normal file
@@ -0,0 +1,61 @@
|
||||
Anchor = require 'anchor'
|
||||
Point = require 'point'
|
||||
Range = require 'range'
|
||||
{View, $$} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class SelectionView extends View
|
||||
@content: ->
|
||||
@div()
|
||||
|
||||
regions: null
|
||||
|
||||
initialize: ({@editor, @selection} = {}) ->
|
||||
@regions = []
|
||||
@selection.on 'change-screen-range', => @updateAppearance()
|
||||
@selection.on 'destroy', => @remove()
|
||||
@updateAppearance()
|
||||
|
||||
updateAppearance: ->
|
||||
@clearRegions()
|
||||
range = @getScreenRange()
|
||||
|
||||
@editor.highlightFoldsContainingBufferRange(@getBufferRange())
|
||||
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: ->
|
||||
@selection.getScreenRange()
|
||||
|
||||
getBufferRange: ->
|
||||
@selection.getBufferRange()
|
||||
|
||||
remove: ->
|
||||
@editor.removeSelectionView(this)
|
||||
super
|
||||
256
src/app/selection.coffee
Normal file
256
src/app/selection.coffee
Normal file
@@ -0,0 +1,256 @@
|
||||
Range = require 'range'
|
||||
Anchor = require 'anchor'
|
||||
EventEmitter = require 'event-emitter'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Selection
|
||||
anchor: null
|
||||
|
||||
constructor: ({@cursor, @editSession}) ->
|
||||
@cursor.selection = this
|
||||
|
||||
@cursor.on 'change-screen-position.selection', (e) =>
|
||||
@trigger 'change-screen-range', @getScreenRange() unless e.bufferChanged
|
||||
|
||||
@cursor.on 'destroy.selection', =>
|
||||
@cursor = null
|
||||
@destroy()
|
||||
|
||||
destroy: ->
|
||||
if @cursor
|
||||
@cursor.off('.selection')
|
||||
@cursor.destroy()
|
||||
@anchor?.destroy()
|
||||
@editSession.removeSelection(this)
|
||||
@trigger 'destroy'
|
||||
|
||||
isEmpty: ->
|
||||
@getBufferRange().isEmpty()
|
||||
|
||||
isReversed: ->
|
||||
not @isEmpty() and @cursor.getBufferPosition().isLessThan(@anchor.getBufferPosition())
|
||||
|
||||
getScreenRange: ->
|
||||
if @anchor
|
||||
new Range(@anchor.getScreenPosition(), @cursor.getScreenPosition())
|
||||
else
|
||||
new Range(@cursor.getScreenPosition(), @cursor.getScreenPosition())
|
||||
|
||||
setScreenRange: (screenRange, options={}) ->
|
||||
screenRange = Range.fromObject(screenRange)
|
||||
{ start, end } = screenRange
|
||||
[start, end] = [end, start] if options.reverse
|
||||
|
||||
@modifyScreenRange =>
|
||||
@placeAnchor() unless @anchor
|
||||
@modifySelection =>
|
||||
@anchor.setScreenPosition(start)
|
||||
@cursor.setScreenPosition(end)
|
||||
|
||||
setBufferRange: (bufferRange, options={}) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
{ start, end } = bufferRange
|
||||
[start, end] = [end, start] if options.reverse
|
||||
|
||||
@modifyScreenRange =>
|
||||
@editSession.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
||||
@placeAnchor() unless @anchor
|
||||
@modifySelection =>
|
||||
@anchor.setBufferPosition(start, options)
|
||||
@cursor.setBufferPosition(end, options)
|
||||
|
||||
getBufferRange: ->
|
||||
if @anchor
|
||||
new Range(@anchor.getBufferPosition(), @cursor.getBufferPosition())
|
||||
else
|
||||
new Range(@cursor.getBufferPosition(), @cursor.getBufferPosition())
|
||||
|
||||
getText: ->
|
||||
@editSession.buffer.getTextInRange(@getBufferRange())
|
||||
|
||||
clear: ->
|
||||
@modifyScreenRange =>
|
||||
@anchor?.destroy()
|
||||
@anchor = null
|
||||
|
||||
selectWord: ->
|
||||
@setBufferRange(@cursor.getCurrentWordBufferRange())
|
||||
|
||||
expandOverWord: ->
|
||||
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()))
|
||||
|
||||
selectLine: (row=@cursor.getBufferPosition().row) ->
|
||||
@setBufferRange(@editSession.bufferRangeForBufferRow(row))
|
||||
|
||||
expandOverLine: ->
|
||||
@setBufferRange(@getBufferRange().union(@cursor.getCurrentLineBufferRange()))
|
||||
|
||||
selectToScreenPosition: (position) ->
|
||||
@modifySelection => @cursor.setScreenPosition(position)
|
||||
|
||||
selectToBufferPosition: (position) ->
|
||||
@modifySelection => @cursor.setBufferPosition(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()
|
||||
|
||||
selectAll: ->
|
||||
@setBufferRange(@editSession.buffer.getRange())
|
||||
|
||||
selectToBeginningOfLine: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfLine()
|
||||
|
||||
selectToEndOfLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfLine()
|
||||
|
||||
selectToBeginningOfWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfWord()
|
||||
|
||||
selectToEndOfWord: ->
|
||||
@modifySelection => @cursor.moveToEndOfWord()
|
||||
|
||||
insertText: (text, options={}) ->
|
||||
oldBufferRange = @getBufferRange()
|
||||
@editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row)
|
||||
wasReversed = @isReversed()
|
||||
@clear()
|
||||
newBufferRange = @editSession.buffer.change(oldBufferRange, text)
|
||||
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
|
||||
|
||||
autoIndent = options.autoIndent ? true
|
||||
|
||||
if @editSession.autoIndent and autoIndent
|
||||
if /\n/.test(text)
|
||||
firstLinePrefix = @editSession.getTextInBufferRange([[newBufferRange.start.row, 0], newBufferRange.start])
|
||||
if /^\s*$/.test(firstLinePrefix)
|
||||
@editSession.autoIncreaseIndentForBufferRow(newBufferRange.start.row)
|
||||
|
||||
if newBufferRange.getRowCount() > 1
|
||||
@editSession.autoIndentBufferRows(newBufferRange.start.row + 1, newBufferRange.end.row)
|
||||
else
|
||||
@editSession.autoDecreaseIndentForRow(newBufferRange.start.row)
|
||||
|
||||
backspace: ->
|
||||
if @isEmpty() and not @editSession.isFoldedAtScreenRow(@cursor.getScreenRow())
|
||||
if @cursor.isAtBeginningOfLine() and @editSession.isFoldedAtScreenRow(@cursor.getScreenRow() - 1)
|
||||
@selectToBufferPosition([@cursor.getBufferRow() - 1, Infinity])
|
||||
else
|
||||
@selectLeft()
|
||||
|
||||
@deleteSelectedText()
|
||||
|
||||
backspaceToBeginningOfWord: ->
|
||||
@selectToBeginningOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
delete: ->
|
||||
if @isEmpty()
|
||||
if @cursor.isAtEndOfLine() and fold = @editSession.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1)
|
||||
@selectToBufferPosition(fold.getBufferRange().end)
|
||||
else
|
||||
@selectRight()
|
||||
@deleteSelectedText()
|
||||
|
||||
deleteToEndOfWord: ->
|
||||
@selectToEndOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
deleteSelectedText: ->
|
||||
bufferRange = @getBufferRange()
|
||||
if fold = @editSession.largestFoldContainingBufferRow(bufferRange.end.row)
|
||||
includeNewline = bufferRange.start.column == 0 or bufferRange.start.row >= fold.startRow
|
||||
bufferRange = bufferRange.union(fold.getBufferRange({ includeNewline }))
|
||||
|
||||
@modifyScreenRange =>
|
||||
@editSession.buffer.delete(bufferRange) unless bufferRange.isEmpty()
|
||||
@cursor?.setBufferPosition(bufferRange.start)
|
||||
|
||||
indentSelectedRows: ->
|
||||
range = @getBufferRange()
|
||||
for row in [range.start.row..range.end.row]
|
||||
@editSession.buffer.insert([row, 0], @editSession.tabText) unless @editSession.buffer.lineLengthForRow(row) == 0
|
||||
|
||||
outdentSelectedRows: ->
|
||||
range = @getBufferRange()
|
||||
buffer = @editSession.buffer
|
||||
leadingTabRegex = new RegExp("^#{@editSession.tabText}")
|
||||
for row in [range.start.row..range.end.row]
|
||||
if leadingTabRegex.test buffer.lineForRow(row)
|
||||
buffer.delete [[row, 0], [row, @editSession.tabText.length]]
|
||||
|
||||
toggleLineComments: ->
|
||||
@modifySelection =>
|
||||
@editSession.toggleLineCommentsInRange(@getBufferRange())
|
||||
|
||||
cutToEndOfLine: (maintainPasteboard) ->
|
||||
@selectToEndOfLine() if @isEmpty()
|
||||
@cut(maintainPasteboard)
|
||||
|
||||
cut: (maintainPasteboard=false) ->
|
||||
@copy(maintainPasteboard)
|
||||
@delete()
|
||||
|
||||
copy: (maintainPasteboard=false) ->
|
||||
return if @isEmpty()
|
||||
text = @editSession.buffer.getTextInRange(@getBufferRange())
|
||||
text = $native.readFromPasteboard() + "\n" + text if maintainPasteboard
|
||||
$native.writeToPasteboard text
|
||||
|
||||
fold: ->
|
||||
range = @getBufferRange()
|
||||
@editSession.createFold(range.start.row, range.end.row)
|
||||
@cursor.setBufferPosition([range.end.row + 1, 0])
|
||||
|
||||
autoIndentText: (text) ->
|
||||
@editSession.autoIndentTextAfterBufferPosition(text, @cursor.getBufferPosition())
|
||||
|
||||
autoOutdent: ->
|
||||
@editSession.autoOutdentBufferRow(@cursor.getBufferRow())
|
||||
|
||||
modifySelection: (fn) ->
|
||||
@retainSelection = true
|
||||
@view?.retainSelection = true
|
||||
@placeAnchor() unless @anchor
|
||||
fn()
|
||||
@retainSelection = false
|
||||
@view?.retainSelection = false
|
||||
|
||||
modifyScreenRange: (fn) ->
|
||||
oldScreenRange = @getScreenRange()
|
||||
fn()
|
||||
if @cursor
|
||||
newScreenRange = @getScreenRange()
|
||||
@trigger 'change-screen-range', newScreenRange unless oldScreenRange.isEqual(newScreenRange)
|
||||
|
||||
placeAnchor: ->
|
||||
@anchor = @editSession.addAnchor(strong: true)
|
||||
@anchor.setScreenPosition(@cursor.getScreenPosition())
|
||||
@anchor.on 'change-screen-position.selection', => @trigger 'change-screen-range'
|
||||
|
||||
intersectsBufferRange: (bufferRange) ->
|
||||
@getBufferRange().intersectsWith(bufferRange)
|
||||
|
||||
intersectsWith: (otherSelection) ->
|
||||
@getScreenRange().intersectsWith(otherSelection.getScreenRange())
|
||||
|
||||
merge: (otherSelection, options) ->
|
||||
@setScreenRange(@getScreenRange().union(otherSelection.getScreenRange()), options)
|
||||
otherSelection.destroy()
|
||||
|
||||
_.extend Selection.prototype, EventEmitter
|
||||
40
src/app/status-bar.coffee
Normal file
40
src/app/status-bar.coffee
Normal file
@@ -0,0 +1,40 @@
|
||||
{View} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class StatusBar extends View
|
||||
@activate: (rootView) ->
|
||||
requireStylesheet 'status-bar.css'
|
||||
|
||||
for editor in rootView.getEditors()
|
||||
@appendToEditorPane(rootView, editor) if rootView.parents('html').length
|
||||
|
||||
rootView.on 'editor-open', (e, editor) =>
|
||||
@appendToEditorPane(rootView, editor)
|
||||
|
||||
@appendToEditorPane: (rootView, editor) ->
|
||||
if pane = editor.pane()
|
||||
pane.append(new StatusBar(rootView, editor))
|
||||
|
||||
@content: ->
|
||||
@div class: 'status-bar', =>
|
||||
@div class: 'current-path', outlet: 'currentPath'
|
||||
@div class: 'cursor-position', outlet: 'cursorPosition'
|
||||
|
||||
initialize: (@rootView, @editor) ->
|
||||
@updatePathText()
|
||||
@editor.on 'editor-path-change', => @updatePathText()
|
||||
|
||||
@updateCursorPositionText()
|
||||
@editor.on 'cursor-move', => @updateCursorPositionText()
|
||||
|
||||
updatePathText: ->
|
||||
path = @editor.getPath()
|
||||
if path
|
||||
@currentPath.text(@rootView.project.relativize(path))
|
||||
else
|
||||
@currentPath.text('untitled')
|
||||
|
||||
updateCursorPositionText: ->
|
||||
{ row, column } = @editor.getCursorBufferPosition()
|
||||
@cursorPosition.text("#{row + 1},#{column + 1}")
|
||||
|
||||
71
src/app/text-mate-bundle.coffee
Normal file
71
src/app/text-mate-bundle.coffee
Normal file
@@ -0,0 +1,71 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
plist = require 'plist'
|
||||
|
||||
TextMateGrammar = require 'text-mate-grammar'
|
||||
|
||||
module.exports =
|
||||
class TextMateBundle
|
||||
@grammarsByFileType: {}
|
||||
@preferencesByScopeSelector: {}
|
||||
@bundles: []
|
||||
|
||||
@loadAll: ->
|
||||
for bundlePath in fs.list(require.resolve("bundles"))
|
||||
@registerBundle(new TextMateBundle(bundlePath))
|
||||
|
||||
@registerBundle: (bundle)->
|
||||
@bundles.push(bundle)
|
||||
|
||||
for scopeSelector, preferences of bundle.getPreferencesByScopeSelector()
|
||||
@preferencesByScopeSelector[scopeSelector] = preferences
|
||||
|
||||
for grammar in bundle.grammars
|
||||
for fileType in grammar.fileTypes
|
||||
@grammarsByFileType[fileType] = grammar
|
||||
|
||||
@grammarForFileName: (fileName) ->
|
||||
extension = fs.extension(fileName)?[1...]
|
||||
@grammarsByFileType[extension] or @grammarsByFileType["txt"]
|
||||
|
||||
@getPreferenceInScope: (scopeSelector, preferenceName) ->
|
||||
@preferencesByScopeSelector[scopeSelector]?[preferenceName]
|
||||
|
||||
@lineCommentStringForScope: (scope) ->
|
||||
shellVariables = @getPreferenceInScope(scope, 'shellVariables')
|
||||
lineComment = (_.find shellVariables, ({name}) -> name == "TM_COMMENT_START")['value']
|
||||
|
||||
@indentRegexForScope: (scope) ->
|
||||
if source = @getPreferenceInScope(scope, 'increaseIndentPattern')
|
||||
new OnigRegExp(source)
|
||||
|
||||
@outdentRegexForScope: (scope) ->
|
||||
if source = @getPreferenceInScope(scope, 'decreaseIndentPattern')
|
||||
new OnigRegExp(source)
|
||||
|
||||
grammars: null
|
||||
|
||||
constructor: (@path) ->
|
||||
@grammars = []
|
||||
if fs.exists(@getSyntaxesPath())
|
||||
for syntaxPath in fs.list(@getSyntaxesPath())
|
||||
@grammars.push TextMateGrammar.loadFromPath(syntaxPath)
|
||||
|
||||
getPreferencesByScopeSelector: ->
|
||||
return {} unless fs.exists(@getPreferencesPath())
|
||||
preferencesByScopeSelector = {}
|
||||
for preferencePath in fs.list(@getPreferencesPath())
|
||||
plist.parseString fs.read(preferencePath), (e, data) ->
|
||||
throw new Error(e) if e
|
||||
{ scope, settings } = data[0]
|
||||
|
||||
preferencesByScopeSelector[scope] = _.extend(preferencesByScopeSelector[scope] ? {}, settings)
|
||||
|
||||
preferencesByScopeSelector
|
||||
|
||||
getSyntaxesPath: ->
|
||||
fs.join(@path, "Syntaxes")
|
||||
|
||||
getPreferencesPath: ->
|
||||
fs.join(@path, "Preferences")
|
||||
|
||||
212
src/app/text-mate-grammar.coffee
Normal file
212
src/app/text-mate-grammar.coffee
Normal file
@@ -0,0 +1,212 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
plist = require 'plist'
|
||||
|
||||
module.exports =
|
||||
class TextMateGrammar
|
||||
@loadFromPath: (path) ->
|
||||
grammar = null
|
||||
plist.parseString fs.read(path), (e, data) ->
|
||||
throw new Error(e) if e
|
||||
grammar = new TextMateGrammar(data[0])
|
||||
grammar
|
||||
|
||||
name: null
|
||||
fileTypes: null
|
||||
foldEndRegex: null
|
||||
repository: null
|
||||
initialRule: null
|
||||
|
||||
constructor: ({ @name, @fileTypes, scopeName, patterns, repository, foldingStopMarker}) ->
|
||||
@initialRule = new Rule(this, {scopeName, patterns})
|
||||
@repository = {}
|
||||
@foldEndRegex = new OnigRegExp(foldingStopMarker) if foldingStopMarker
|
||||
|
||||
for name, data of repository
|
||||
@repository[name] = new Rule(this, data)
|
||||
|
||||
for rule in [@initialRule, _.values(@repository)...]
|
||||
rule.compileRegex()
|
||||
|
||||
getLineTokens: (line, stack=[@initialRule]) ->
|
||||
stack = new Array(stack...)
|
||||
tokens = []
|
||||
position = 0
|
||||
|
||||
loop
|
||||
scopes = _.pluck(stack, "scopeName")
|
||||
|
||||
if line.length == 0
|
||||
tokens = [{value: "", scopes: scopes}]
|
||||
return { tokens, scopes }
|
||||
|
||||
break if position == line.length
|
||||
|
||||
{ nextTokens, tokensStartPosition, tokensEndPosition} = _.last(stack).getNextTokens(stack, line, position)
|
||||
|
||||
if nextTokens
|
||||
if position < tokensStartPosition # unmatched text preceding next tokens
|
||||
tokens.push
|
||||
value: line[position...tokensStartPosition]
|
||||
scopes: scopes
|
||||
|
||||
tokens.push(nextTokens...)
|
||||
position = tokensEndPosition
|
||||
else
|
||||
tokens.push
|
||||
value: line[position...line.length]
|
||||
scopes: scopes
|
||||
break
|
||||
|
||||
{ tokens, stack }
|
||||
|
||||
ruleForInclude: (name) ->
|
||||
if name[0] == "#"
|
||||
@repository[name[1..]]
|
||||
else if name == "$self"
|
||||
@initialRule
|
||||
|
||||
class Rule
|
||||
grammar: null
|
||||
scopeName: null
|
||||
patterns: null
|
||||
endPattern: null
|
||||
|
||||
constructor: (@grammar, {@scopeName, patterns, @endPattern}) ->
|
||||
patterns ?= []
|
||||
@patterns = []
|
||||
@patterns.push(@endPattern) if @endPattern
|
||||
@patterns.push((patterns.map (pattern) => new Pattern(grammar, pattern))...)
|
||||
|
||||
compileRegex: ->
|
||||
regexComponents = []
|
||||
@patternsByCaptureIndex = {}
|
||||
currentCaptureIndex = 1
|
||||
for [regex, pattern] in @getRegexPatternPairs()
|
||||
regexComponents.push(regex.source)
|
||||
@patternsByCaptureIndex[currentCaptureIndex] = pattern
|
||||
currentCaptureIndex += 1 + regex.getCaptureCount()
|
||||
@regex = new OnigRegExp('(' + regexComponents.join(')|(') + ')')
|
||||
|
||||
pattern.compileRegex() for pattern in @patterns
|
||||
|
||||
getRegexPatternPairs: (included=[]) ->
|
||||
return [] if _.include(included, this)
|
||||
included.push(this)
|
||||
regexPatternPairs = []
|
||||
|
||||
regexPatternPairs.push(@endPattern.getRegexPatternPairs()...) if @endPattern
|
||||
for pattern in @patterns
|
||||
regexPatternPairs.push(pattern.getRegexPatternPairs(included)...)
|
||||
regexPatternPairs
|
||||
|
||||
getNextTokens: (stack, line, position) ->
|
||||
captureIndices = @regex.getCaptureIndices(line, position)
|
||||
|
||||
return {} unless captureIndices?[2] > 0 # ignore zero-length matches
|
||||
|
||||
shiftCapture(captureIndices)
|
||||
|
||||
[firstCaptureIndex, firstCaptureStart, firstCaptureEnd] = captureIndices
|
||||
pattern = @patternsByCaptureIndex[firstCaptureIndex]
|
||||
nextTokens = pattern.handleMatch(stack, line, captureIndices)
|
||||
{ nextTokens, tokensStartPosition: firstCaptureStart, tokensEndPosition: firstCaptureEnd }
|
||||
|
||||
getNextMatch: (line, position) ->
|
||||
nextMatch = null
|
||||
matchedPattern = null
|
||||
|
||||
for pattern in @patterns
|
||||
{ pattern, match } = pattern.getNextMatch(line, position)
|
||||
if match
|
||||
if !nextMatch or match.position < nextMatch.position
|
||||
nextMatch = match
|
||||
matchedPattern = pattern
|
||||
|
||||
{ match: nextMatch, pattern: matchedPattern }
|
||||
|
||||
class Pattern
|
||||
grammar: null
|
||||
pushRule: null
|
||||
popRule: false
|
||||
scopeName: null
|
||||
regex: null
|
||||
captures: null
|
||||
|
||||
constructor: (@grammar, { name, contentName, @include, match, begin, end, captures, beginCaptures, endCaptures, patterns, @popRule}) ->
|
||||
@scopeName = name ? contentName # TODO: We need special treatment of contentName
|
||||
if match
|
||||
@regex = new OnigRegExp(match)
|
||||
@captures = captures
|
||||
else if begin
|
||||
@regex = new OnigRegExp(begin)
|
||||
@captures = beginCaptures ? captures
|
||||
endPattern = new Pattern(@grammar, { match: end, captures: endCaptures ? captures, popRule: true})
|
||||
@pushRule = new Rule(@grammar, { @scopeName, patterns, endPattern })
|
||||
|
||||
getRegexPatternPairs: (included) ->
|
||||
if @include
|
||||
rule = @grammar.ruleForInclude(@include)
|
||||
# console.log "Could not find rule for include #{@include} in #{@grammar.name} grammar" unless rule
|
||||
rule?.getRegexPatternPairs(included) ? []
|
||||
else
|
||||
[[@regex, this]]
|
||||
|
||||
compileRegex: ->
|
||||
@pushRule?.compileRegex()
|
||||
|
||||
getNextMatch: (line, position) ->
|
||||
if @include
|
||||
rule = @grammar.ruleForInclude(@include)
|
||||
rule.getNextMatch(line, position)
|
||||
else
|
||||
{ match: @regex.getCaptureIndices(line, position), pattern: this }
|
||||
|
||||
handleMatch: (stack, line, captureIndices) ->
|
||||
scopes = _.pluck(stack, "scopeName")
|
||||
scopes.push(@scopeName) unless @popRule
|
||||
|
||||
if @captures
|
||||
tokens = @getTokensForCaptureIndices(line, captureIndices, scopes)
|
||||
else
|
||||
[start, end] = captureIndices[1..2]
|
||||
tokens = [{ value: line[start...end], scopes: scopes }]
|
||||
|
||||
if @pushRule
|
||||
stack.push(@pushRule)
|
||||
else if @popRule
|
||||
stack.pop()
|
||||
|
||||
tokens
|
||||
|
||||
getTokensForCaptureIndices: (line, captureIndices, scopes, indexOffset=captureIndices[0]) ->
|
||||
[parentCaptureIndex, parentCaptureStart, parentCaptureEnd] = shiftCapture(captureIndices)
|
||||
relativeParentCaptureIndex = parentCaptureIndex - indexOffset
|
||||
|
||||
tokens = []
|
||||
if scope = @captures[relativeParentCaptureIndex]?.name
|
||||
scopes = scopes.concat(scope)
|
||||
|
||||
previousChildCaptureEnd = parentCaptureStart
|
||||
while captureIndices.length and captureIndices[1] < parentCaptureEnd
|
||||
[childCaptureIndex, childCaptureStart, childCaptureEnd] = captureIndices
|
||||
|
||||
if childCaptureStart > previousChildCaptureEnd
|
||||
tokens.push
|
||||
value: line[previousChildCaptureEnd...childCaptureStart]
|
||||
scopes: scopes
|
||||
|
||||
captureTokens = @getTokensForCaptureIndices(line, captureIndices, scopes, indexOffset)
|
||||
tokens.push(captureTokens...)
|
||||
previousChildCaptureEnd = childCaptureEnd
|
||||
|
||||
if parentCaptureEnd > previousChildCaptureEnd
|
||||
tokens.push
|
||||
value: line[previousChildCaptureEnd...parentCaptureEnd]
|
||||
scopes: scopes
|
||||
|
||||
tokens
|
||||
|
||||
shiftCapture = (captureIndices) ->
|
||||
[captureIndices.shift(), captureIndices.shift(), captureIndices.shift()]
|
||||
|
||||
108
src/app/text-mate-theme.coffee
Normal file
108
src/app/text-mate-theme.coffee
Normal file
@@ -0,0 +1,108 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
plist = require 'plist'
|
||||
|
||||
module.exports =
|
||||
class TextMateTheme
|
||||
@themesByName: {}
|
||||
|
||||
@loadAll: ->
|
||||
for themePath in fs.list(require.resolve("themes"))
|
||||
@registerTheme(TextMateTheme.load(themePath))
|
||||
|
||||
@load: (path) ->
|
||||
plistString = fs.read(require.resolve(path))
|
||||
theme = null
|
||||
plist.parseString plistString, (err, data) ->
|
||||
throw new Error("Error loading theme at '#{path}': #{err}") if err
|
||||
theme = new TextMateTheme(data[0])
|
||||
theme
|
||||
|
||||
@registerTheme: (theme) ->
|
||||
@themesByName[theme.name] = theme
|
||||
|
||||
@getNames: ->
|
||||
_.keys(@themesByName)
|
||||
|
||||
@getTheme: (name) ->
|
||||
@themesByName[name]
|
||||
|
||||
@activate: (name) ->
|
||||
if theme = @getTheme(name)
|
||||
theme.activate()
|
||||
else
|
||||
throw new Error("No theme with name '#{name}'")
|
||||
|
||||
constructor: ({@name, settings}) ->
|
||||
@rulesets = []
|
||||
globalSettings = settings[0]
|
||||
@buildGlobalSettingsRulesets(settings[0])
|
||||
@buildScopeSelectorRulesets(settings[1..])
|
||||
|
||||
activate: ->
|
||||
applyStylesheet(@name, @getStylesheet())
|
||||
|
||||
getStylesheet: ->
|
||||
lines = []
|
||||
for {selector, properties} in @getRulesets()
|
||||
lines.push("#{selector} {")
|
||||
for name, value of properties
|
||||
lines.push " #{name}: #{value};"
|
||||
lines.push("}\n")
|
||||
lines.join("\n")
|
||||
|
||||
getRulesets: -> @rulesets
|
||||
|
||||
buildGlobalSettingsRulesets: ({settings}) ->
|
||||
{ background, foreground, caret, selection } = settings
|
||||
|
||||
@rulesets.push
|
||||
selector: '.editor'
|
||||
properties:
|
||||
'background-color': @translateColor(background)
|
||||
'color': @translateColor(foreground)
|
||||
|
||||
@rulesets.push
|
||||
selector: '.editor.focused .cursor'
|
||||
properties:
|
||||
'border-color': @translateColor(caret)
|
||||
|
||||
@rulesets.push
|
||||
selector: '.editor.focused .selection'
|
||||
properties:
|
||||
'background-color': @translateColor(selection)
|
||||
|
||||
buildScopeSelectorRulesets: (scopeSelectorSettings) ->
|
||||
for { name, scope, settings } in scopeSelectorSettings
|
||||
continue unless scope
|
||||
@rulesets.push
|
||||
comment: name
|
||||
selector: @translateScopeSelector(scope)
|
||||
properties: @translateScopeSelectorSettings(settings)
|
||||
|
||||
translateScopeSelector: (textmateScopeSelector) ->
|
||||
scopes = textmateScopeSelector.replace(/\./g, '-').split(/\s+/).map (scope) -> '.' + scope
|
||||
scopes.join(' ')
|
||||
|
||||
translateScopeSelectorSettings: ({ foreground, background, fontStyle }) ->
|
||||
properties = {}
|
||||
|
||||
if fontStyle
|
||||
fontStyles = fontStyle.split(/\s+/)
|
||||
# properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold')
|
||||
# properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic')
|
||||
properties['text-decoration'] = 'underline' if _.contains(fontStyles, 'underline')
|
||||
|
||||
properties['color'] = @translateColor(foreground) if foreground
|
||||
properties['background-color'] = @translateColor(background) if background
|
||||
properties
|
||||
|
||||
translateColor: (textmateColor) ->
|
||||
if textmateColor.length <= 7
|
||||
textmateColor
|
||||
else
|
||||
r = parseInt(textmateColor[1..2], 16)
|
||||
g = parseInt(textmateColor[3..4], 16)
|
||||
b = parseInt(textmateColor[5..6], 16)
|
||||
a = parseInt(textmateColor[7..8], 16)
|
||||
"rgba(#{r}, #{g}, #{b}, #{a})"
|
||||
46
src/app/token.coffee
Normal file
46
src/app/token.coffee
Normal file
@@ -0,0 +1,46 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Token
|
||||
value: null
|
||||
scopes: null
|
||||
isAtomic: null
|
||||
|
||||
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold}) ->
|
||||
@screenDelta = @value.length
|
||||
@bufferDelta ?= @screenDelta
|
||||
|
||||
isEqual: (other) ->
|
||||
@value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic
|
||||
|
||||
isBracket: ->
|
||||
/^meta\.brace\b/.test(_.last(@scopes))
|
||||
|
||||
splitAt: (splitIndex) ->
|
||||
value1 = @value.substring(0, splitIndex)
|
||||
value2 = @value.substring(splitIndex)
|
||||
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
|
||||
|
||||
breakOutTabCharacters: (tabText) ->
|
||||
return [this] unless /\t/.test(@value)
|
||||
|
||||
for substring in @value.match(/([^\t]+|\t)/g)
|
||||
if substring == '\t'
|
||||
@buildTabToken(tabText)
|
||||
else
|
||||
new Token(value: substring, scopes: @scopes)
|
||||
|
||||
buildTabToken: (tabText) ->
|
||||
new Token(value: tabText, scopes: @scopes, bufferDelta: 1, isAtomic: true)
|
||||
|
||||
getCssClassString: ->
|
||||
@cssClassString ?= @getCssClasses().join(' ')
|
||||
|
||||
getCssClasses: ->
|
||||
classes = []
|
||||
for scope in @scopes
|
||||
scopeComponents = scope.split('.')
|
||||
for i in [0...scopeComponents.length]
|
||||
classes.push scopeComponents[0..i].join('-')
|
||||
classes
|
||||
|
||||
153
src/app/tokenized-buffer.coffee
Normal file
153
src/app/tokenized-buffer.coffee
Normal file
@@ -0,0 +1,153 @@
|
||||
_ = require 'underscore'
|
||||
ScreenLine = require 'screen-line'
|
||||
EventEmitter = require 'event-emitter'
|
||||
Token = require 'token'
|
||||
Range = require 'range'
|
||||
Point = require 'point'
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer
|
||||
@idCounter: 1
|
||||
|
||||
languageMode: null
|
||||
buffer: null
|
||||
aceAdaptor: null
|
||||
screenLines: null
|
||||
|
||||
constructor: (@buffer, { @languageMode, @tabText }) ->
|
||||
@languageMode.tokenizedBuffer = this
|
||||
@id = @constructor.idCounter++
|
||||
@screenLines = @buildScreenLinesForRows(0, @buffer.getLastRow())
|
||||
@buffer.on "change.tokenized-buffer#{@id}", (e) => @handleBufferChange(e)
|
||||
|
||||
handleBufferChange: (e) ->
|
||||
oldRange = e.oldRange.copy()
|
||||
newRange = e.newRange.copy()
|
||||
previousStack = @stackForRow(oldRange.end.row) # used in spill detection below
|
||||
|
||||
stack = @stackForRow(newRange.start.row - 1)
|
||||
@screenLines[oldRange.start.row..oldRange.end.row] =
|
||||
@buildScreenLinesForRows(newRange.start.row, newRange.end.row, stack)
|
||||
|
||||
# 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 _.isEqual(@stackForRow(row), previousStack)
|
||||
nextRow = row + 1
|
||||
previousStack = @stackForRow(nextRow)
|
||||
@screenLines[nextRow] = @buildScreenLineForRow(nextRow, @stackForRow(row))
|
||||
|
||||
# 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})
|
||||
|
||||
buildScreenLinesForRows: (startRow, endRow, startingStack) ->
|
||||
stack = startingStack
|
||||
for row in [startRow..endRow]
|
||||
screenLine = @buildScreenLineForRow(row, stack)
|
||||
stack = screenLine.stack
|
||||
screenLine
|
||||
|
||||
buildScreenLineForRow: (row, stack) ->
|
||||
line = @buffer.lineForRow(row)
|
||||
{tokens, stack} = @languageMode.getLineTokens(line, stack)
|
||||
tokenObjects = []
|
||||
for tokenProperties in tokens
|
||||
token = new Token(tokenProperties)
|
||||
tokenObjects.push(token.breakOutTabCharacters(@tabText)...)
|
||||
text = _.pluck(tokenObjects, 'value').join('')
|
||||
new ScreenLine(tokenObjects, text, [1, 0], [1, 0], { stack })
|
||||
|
||||
lineForScreenRow: (row) ->
|
||||
@screenLines[row]
|
||||
|
||||
linesForScreenRows: (startRow, endRow) ->
|
||||
@screenLines[startRow..endRow]
|
||||
|
||||
stackForRow: (row) ->
|
||||
@screenLines[row]?.stack
|
||||
|
||||
scopesForPosition: (position) ->
|
||||
position = Point.fromObject(position)
|
||||
token = @screenLines[position.row].tokenAtBufferColumn(position.column)
|
||||
token.scopes
|
||||
|
||||
destroy: ->
|
||||
@buffer.off ".tokenized-buffer#{@id}"
|
||||
|
||||
iterateTokensInBufferRange: (bufferRange, iterator) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
{ start, end } = bufferRange
|
||||
|
||||
keepLooping = true
|
||||
stop = -> keepLooping = false
|
||||
|
||||
for bufferRow in [start.row..end.row]
|
||||
bufferColumn = 0
|
||||
for token in @screenLines[bufferRow].tokens
|
||||
startOfToken = new Point(bufferRow, bufferColumn)
|
||||
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
|
||||
return unless keepLooping
|
||||
bufferColumn += token.bufferDelta
|
||||
|
||||
backwardsIterateTokensInBufferRange: (bufferRange, iterator) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
{ start, end } = bufferRange
|
||||
|
||||
keepLooping = true
|
||||
stop = -> keepLooping = false
|
||||
|
||||
for bufferRow in [end.row..start.row]
|
||||
bufferColumn = @buffer.lineLengthForRow(bufferRow)
|
||||
for token in new Array(@screenLines[bufferRow].tokens...).reverse()
|
||||
bufferColumn -= token.bufferDelta
|
||||
startOfToken = new Point(bufferRow, bufferColumn)
|
||||
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
|
||||
return unless keepLooping
|
||||
|
||||
findOpeningBracket: (startBufferPosition) ->
|
||||
range = [[0,0], startBufferPosition]
|
||||
position = null
|
||||
depth = 0
|
||||
@backwardsIterateTokensInBufferRange range, (token, startPosition, { stop }) ->
|
||||
if token.isBracket()
|
||||
if token.value == '}'
|
||||
depth++
|
||||
else if token.value == '{'
|
||||
depth--
|
||||
if depth == 0
|
||||
position = startPosition
|
||||
stop()
|
||||
position
|
||||
|
||||
findClosingBracket: (startBufferPosition) ->
|
||||
range = [startBufferPosition, @buffer.getEofPosition()]
|
||||
position = null
|
||||
depth = 0
|
||||
@iterateTokensInBufferRange range, (token, startPosition, { stop }) ->
|
||||
if token.isBracket()
|
||||
if token.value == '{'
|
||||
depth++
|
||||
else if token.value == '}'
|
||||
depth--
|
||||
if depth == 0
|
||||
position = startPosition
|
||||
stop()
|
||||
position
|
||||
|
||||
logLines: (start=0, end=@buffer.getLastRow()) ->
|
||||
for row in [start..end]
|
||||
line = @lineForScreenRow(row).text
|
||||
console.log row, line, line.length
|
||||
|
||||
_.extend(TokenizedBuffer.prototype, EventEmitter)
|
||||
46
src/app/undo-manager.coffee
Normal file
46
src/app/undo-manager.coffee
Normal file
@@ -0,0 +1,46 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
|
||||
class UndoManager
|
||||
undoHistory: null
|
||||
redoHistory: null
|
||||
currentTransaction: null
|
||||
|
||||
constructor: ->
|
||||
@startBatchCallCount = 0
|
||||
@undoHistory = []
|
||||
@redoHistory = []
|
||||
|
||||
pushOperation: (operation, editSession) ->
|
||||
if @currentTransaction
|
||||
@currentTransaction.push(operation)
|
||||
else
|
||||
@undoHistory.push([operation])
|
||||
@redoHistory = []
|
||||
operation.do?(editSession)
|
||||
|
||||
transact: (fn) ->
|
||||
if @currentTransaction
|
||||
fn()
|
||||
else
|
||||
@currentTransaction = []
|
||||
fn()
|
||||
@undoHistory.push(@currentTransaction) if @currentTransaction.length
|
||||
@currentTransaction = null
|
||||
|
||||
undo: (editSession) ->
|
||||
if batch = @undoHistory.pop()
|
||||
opsInReverse = new Array(batch...)
|
||||
opsInReverse.reverse()
|
||||
op.undo?(editSession) for op in opsInReverse
|
||||
@redoHistory.push batch
|
||||
batch.oldSelectionRanges
|
||||
|
||||
redo: (editSession) ->
|
||||
if batch = @redoHistory.pop()
|
||||
for op in batch
|
||||
op.do?(editSession)
|
||||
op.redo?(editSession)
|
||||
@undoHistory.push(batch)
|
||||
batch.newSelectionRanges
|
||||
108
src/app/window.coffee
Normal file
108
src/app/window.coffee
Normal file
@@ -0,0 +1,108 @@
|
||||
# This a weirdo file. We don't create a Window class, we just add stuff to
|
||||
# the DOM window.
|
||||
|
||||
Native = require 'native'
|
||||
TextMateBundle = require 'text-mate-bundle'
|
||||
TextMateTheme = require 'text-mate-theme'
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
{CoffeeScript} = require 'coffee-script'
|
||||
|
||||
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) ->
|
||||
TextMateBundle.loadAll()
|
||||
TextMateTheme.loadAll()
|
||||
|
||||
@attachRootView(path)
|
||||
$(window).on 'close', => @close()
|
||||
$(window).on 'beforeunload', =>
|
||||
@shutdown()
|
||||
false
|
||||
$(window).focus()
|
||||
atom.windowOpened this
|
||||
|
||||
shutdown: ->
|
||||
@rootView.deactivate()
|
||||
$(window).unbind('focus')
|
||||
$(window).unbind('blur')
|
||||
$(window).off('before')
|
||||
atom.windowClosed this
|
||||
|
||||
# Note: RootView assigns itself on window on initialization so that
|
||||
# window.rootView is available when loading user configuration
|
||||
attachRootView: (pathToOpen) ->
|
||||
if rootViewState = atom.rootViewStates[$windowNumber]
|
||||
RootView.deserialize(JSON.parse(rootViewState))
|
||||
else
|
||||
new RootView(pathToOpen)
|
||||
@rootView.open() unless pathToOpen
|
||||
|
||||
$(@rootViewParentSelector).append @rootView
|
||||
|
||||
requireStylesheet: (path) ->
|
||||
fullPath = require.resolve(path)
|
||||
window.applyStylesheet(fullPath, fs.read(fullPath))
|
||||
|
||||
applyStylesheet: (id, text) ->
|
||||
unless $("head style[id='#{id}']").length
|
||||
$('head').append "<style id='#{id}'>#{text}</style>"
|
||||
|
||||
requireExtension: (name) ->
|
||||
extensionPath = require.resolve name
|
||||
extension = rootView.activateExtension require(extensionPath)
|
||||
|
||||
extensionKeymapPath = fs.join(fs.directory(extensionPath), "keymap.coffee")
|
||||
require extensionKeymapPath if fs.exists(extensionKeymapPath)
|
||||
|
||||
extension
|
||||
|
||||
reload: ->
|
||||
if rootView.getModifiedBuffers().length > 0
|
||||
message = "There are unsaved buffers, reload anyway?"
|
||||
detailedMessage = "You will lose all unsaved changes if you reload"
|
||||
buttons = [
|
||||
["Reload", -> Native.reload()]
|
||||
["Cancel", ->]
|
||||
]
|
||||
|
||||
Native.alert(message, detailedMessage, buttons)
|
||||
else
|
||||
Native.reload()
|
||||
|
||||
toggleDevTools: ->
|
||||
$native.toggleDevTools()
|
||||
|
||||
onerror: ->
|
||||
$native.showDevTools()
|
||||
|
||||
measure: (description, fn) ->
|
||||
start = new Date().getTime()
|
||||
fn()
|
||||
result = new Date().getTime() - start
|
||||
console.log description, result
|
||||
|
||||
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'
|
||||
2
src/atom-bootstrap.coffee
Normal file
2
src/atom-bootstrap.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
Atom = require 'atom'
|
||||
window.atom = new Atom(atom.loadPath, $native)
|
||||
216
src/extensions/autocomplete/autocomplete.coffee
Normal file
216
src/extensions/autocomplete/autocomplete.coffee
Normal file
@@ -0,0 +1,216 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
Range = require 'range'
|
||||
Editor = require 'editor'
|
||||
fuzzyFilter = require 'fuzzy-filter'
|
||||
|
||||
module.exports =
|
||||
class Autocomplete extends View
|
||||
@content: ->
|
||||
@div class: 'autocomplete', tabindex: -1, =>
|
||||
@ol outlet: 'matchesList'
|
||||
@subview 'miniEditor', new Editor(mini: true)
|
||||
|
||||
editor: null
|
||||
miniEditor: null
|
||||
currentBuffer: null
|
||||
wordList: null
|
||||
wordRegex: /\w+/g
|
||||
allMatches: null
|
||||
filteredMatches: null
|
||||
currentMatchIndex: null
|
||||
isAutocompleting: false
|
||||
originalSelectionBufferRange: null
|
||||
originalSelectedText: null
|
||||
|
||||
@activate: (rootView) ->
|
||||
new Autocomplete(editor) for editor in rootView.getEditors()
|
||||
rootView.on 'editor-open', (e, editor) ->
|
||||
editor.autoComplete = new Autocomplete(editor) unless editor.is('.autocomplete .mini')
|
||||
|
||||
initialize: (@editor) ->
|
||||
requireStylesheet 'autocomplete.css'
|
||||
@handleEvents()
|
||||
@setCurrentBuffer(@editor.getBuffer())
|
||||
|
||||
handleEvents: ->
|
||||
@editor.on 'editor-path-change', => @setCurrentBuffer(@editor.getBuffer())
|
||||
@editor.on 'before-remove', => @currentBuffer?.off '.autocomplete'
|
||||
|
||||
@editor.on 'autocomplete:attach', => @attach()
|
||||
@editor.on 'autocomplete:cancel', => @cancel()
|
||||
@on 'autocomplete:confirm', => @confirm()
|
||||
|
||||
@matchesList.on 'mousedown', (e) =>
|
||||
index = $(e.target).attr('index')
|
||||
@selectMatchAtIndex(index) if index?
|
||||
false
|
||||
|
||||
@matchesList.on 'mouseup', =>
|
||||
if @selectedMatch()
|
||||
@confirm()
|
||||
else
|
||||
@cancel()
|
||||
|
||||
@miniEditor.getBuffer().on 'change', (e) =>
|
||||
@filterMatches() if @hasParent()
|
||||
|
||||
@miniEditor.preempt 'move-up', =>
|
||||
@selectPreviousMatch()
|
||||
false
|
||||
|
||||
@miniEditor.preempt 'move-down', =>
|
||||
@selectNextMatch()
|
||||
false
|
||||
|
||||
@miniEditor.preempt 'textInput', (e) =>
|
||||
text = e.originalEvent.data
|
||||
unless text.match(@wordRegex)
|
||||
@confirm()
|
||||
@editor.insertText(text)
|
||||
false
|
||||
|
||||
setCurrentBuffer: (buffer) ->
|
||||
@currentBuffer?.off '.autocomplete'
|
||||
@currentBuffer = buffer
|
||||
@buildWordList()
|
||||
@currentBuffer.on 'change.autocomplete', (e) =>
|
||||
@buildWordList() unless @isAutocompleting
|
||||
|
||||
buildWordList: () ->
|
||||
wordHash = {}
|
||||
matches = @currentBuffer.getText().match(@wordRegex)
|
||||
wordHash[word] ?= true for word in (matches or [])
|
||||
|
||||
@wordList = Object.keys(wordHash)
|
||||
|
||||
confirm: ->
|
||||
@confirmed = true
|
||||
@editor.getSelection().clear()
|
||||
@detach()
|
||||
return unless match = @selectedMatch()
|
||||
position = @editor.getCursorBufferPosition()
|
||||
@editor.setCursorBufferPosition([position.row, position.column + match.suffix.length])
|
||||
|
||||
cancel: ->
|
||||
@detach()
|
||||
@editor.getBuffer().change(@currentMatchBufferRange, @originalSelectedText) if @currentMatchBufferRange
|
||||
@editor.setSelectedBufferRange(@originalSelectionBufferRange)
|
||||
|
||||
attach: ->
|
||||
@confirmed = false
|
||||
@miniEditor.on 'focusout', =>
|
||||
@cancel() unless @confirmed
|
||||
|
||||
@originalSelectedText = @editor.getSelectedText()
|
||||
@originalSelectionBufferRange = @editor.getSelection().getBufferRange()
|
||||
@currentMatchBufferRange = null
|
||||
@allMatches = @findMatchesForCurrentSelection()
|
||||
|
||||
originalCursorPosition = @editor.getCursorScreenPosition()
|
||||
@filterMatches()
|
||||
@editor.append(this)
|
||||
@setPosition(originalCursorPosition)
|
||||
|
||||
@miniEditor.focus()
|
||||
|
||||
detach: ->
|
||||
@miniEditor.off("focusout")
|
||||
super
|
||||
@editor.off(".autocomplete")
|
||||
@editor.focus()
|
||||
@miniEditor.setText('')
|
||||
|
||||
setPosition: (originalCursorPosition) ->
|
||||
{ left, top } = @editor.pixelPositionForScreenPosition(originalCursorPosition)
|
||||
|
||||
top -= @editor.scrollTop()
|
||||
potentialTop = top + @editor.lineHeight
|
||||
potentialBottom = potentialTop + @outerHeight()
|
||||
|
||||
if potentialBottom > @editor.outerHeight()
|
||||
@css(left: left, bottom: @editor.outerHeight() - top, top: 'inherit')
|
||||
else
|
||||
@css(left: left, top: potentialTop, bottom: 'inherit')
|
||||
|
||||
selectPreviousMatch: ->
|
||||
previousIndex = @currentMatchIndex - 1
|
||||
previousIndex = @filteredMatches.length - 1 if previousIndex < 0
|
||||
@selectMatchAtIndex(previousIndex)
|
||||
|
||||
selectNextMatch: ->
|
||||
nextIndex = (@currentMatchIndex + 1) % @filteredMatches.length
|
||||
@selectMatchAtIndex(nextIndex)
|
||||
|
||||
selectMatchAtIndex: (index) ->
|
||||
@currentMatchIndex = index
|
||||
@matchesList.find("li").removeClass "selected"
|
||||
|
||||
liToSelect = @matchesList.find("li:eq(#{index})")
|
||||
liToSelect.addClass "selected"
|
||||
|
||||
topOfLiToSelect = liToSelect.position().top + @matchesList.scrollTop()
|
||||
bottomOfLiToSelect = topOfLiToSelect + liToSelect.outerHeight()
|
||||
if topOfLiToSelect < @matchesList.scrollTop()
|
||||
@matchesList.scrollTop(topOfLiToSelect)
|
||||
else if bottomOfLiToSelect > @matchesList.scrollBottom()
|
||||
@matchesList.scrollBottom(bottomOfLiToSelect)
|
||||
|
||||
@replaceSelectedTextWithMatch @selectedMatch()
|
||||
|
||||
selectedMatch: ->
|
||||
@filteredMatches[@currentMatchIndex]
|
||||
|
||||
filterMatches: ->
|
||||
@filteredMatches = fuzzyFilter(@allMatches, @miniEditor.getText(), key: 'word')
|
||||
@renderMatchList()
|
||||
|
||||
renderMatchList: ->
|
||||
@matchesList.empty()
|
||||
if @filteredMatches.length > 0
|
||||
@matchesList.append($$ -> @li match.word, index: index) for match, index in @filteredMatches
|
||||
else
|
||||
@matchesList.append($$ -> @li "No matches found")
|
||||
|
||||
@selectMatchAtIndex(0) if @filteredMatches.length > 0
|
||||
|
||||
findMatchesForCurrentSelection: ->
|
||||
selection = @editor.getSelection()
|
||||
{prefix, suffix} = @prefixAndSuffixOfSelection(selection)
|
||||
|
||||
if (prefix.length + suffix.length) > 0
|
||||
regex = new RegExp("^#{prefix}(.+)#{suffix}$", "i")
|
||||
currentWord = prefix + @editor.getSelectedText() + suffix
|
||||
for word in @wordList when regex.test(word) and word != currentWord
|
||||
match = regex.exec(word)
|
||||
{prefix, suffix, word, infix: match[1]}
|
||||
else
|
||||
[]
|
||||
|
||||
replaceSelectedTextWithMatch: (match) ->
|
||||
selection = @editor.getSelection()
|
||||
startPosition = selection.getBufferRange().start
|
||||
@isAutocompleting = true
|
||||
@editor.insertText(match.infix)
|
||||
|
||||
@currentMatchBufferRange = [startPosition, [startPosition.row, startPosition.column + match.infix.length]]
|
||||
@editor.setSelectedBufferRange(@currentMatchBufferRange)
|
||||
@isAutocompleting = false
|
||||
|
||||
prefixAndSuffixOfSelection: (selection) ->
|
||||
selectionRange = selection.getBufferRange()
|
||||
lineRange = [[selectionRange.start.row, 0], [selectionRange.end.row, @editor.lineLengthForBufferRow(selectionRange.end.row)]]
|
||||
[prefix, suffix] = ["", ""]
|
||||
|
||||
@currentBuffer.scanInRange @wordRegex, lineRange, (match, range, {stop}) ->
|
||||
stop() if range.start.isGreaterThan(selectionRange.end)
|
||||
|
||||
if range.intersectsWith(selectionRange)
|
||||
prefixOffset = selectionRange.start.column - range.start.column
|
||||
suffixOffset = selectionRange.end.column - range.end.column
|
||||
|
||||
prefix = match[0][0...prefixOffset] if range.start.isLessThan(selectionRange.start)
|
||||
suffix = match[0][suffixOffset..] if range.end.isGreaterThan(selectionRange.end)
|
||||
|
||||
{prefix, suffix}
|
||||
1
src/extensions/autocomplete/index.coffee
Normal file
1
src/extensions/autocomplete/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'autocomplete/autocomplete.coffee'
|
||||
8
src/extensions/autocomplete/keymap.coffee
Normal file
8
src/extensions/autocomplete/keymap.coffee
Normal file
@@ -0,0 +1,8 @@
|
||||
window.keymap.bindKeys '.editor',
|
||||
'escape': 'autocomplete:attach'
|
||||
'ctrl-space': 'autocomplete:attach'
|
||||
|
||||
window.keymap.bindKeys '.autocomplete .editor',
|
||||
'enter': 'autocomplete:confirm'
|
||||
'escape': 'autocomplete:cancel'
|
||||
'ctrl-space': 'autocomplete:cancel'
|
||||
18
src/extensions/command-panel/command-interpreter.coffee
Normal file
18
src/extensions/command-panel/command-interpreter.coffee
Normal file
@@ -0,0 +1,18 @@
|
||||
fs = require 'fs'
|
||||
PEG = require 'pegjs'
|
||||
|
||||
module.exports =
|
||||
class CommandInterpreter
|
||||
constructor: (@project) ->
|
||||
@parser = PEG.buildParser(fs.read(require.resolve 'command-panel/commands.pegjs'))
|
||||
|
||||
eval: (string, activeEditSession) ->
|
||||
compositeCommand = @parser.parse(string)
|
||||
@lastRelativeAddress = compositeCommand if compositeCommand.isRelativeAddress()
|
||||
compositeCommand.execute(@project, activeEditSession)
|
||||
|
||||
repeatRelativeAddress: (activeEditSession) ->
|
||||
@lastRelativeAddress?.execute(@project, activeEditSession)
|
||||
|
||||
repeatRelativeAddressInReverse: (activeEditSession) ->
|
||||
@lastRelativeAddress?.reverse().execute(@project, activeEditSession)
|
||||
138
src/extensions/command-panel/command-panel.coffee
Normal file
138
src/extensions/command-panel/command-panel.coffee
Normal file
@@ -0,0 +1,138 @@
|
||||
{View, $$$} = require 'space-pen'
|
||||
CommandInterpreter = require 'command-panel/command-interpreter'
|
||||
RegexAddress = require 'command-panel/commands/regex-address'
|
||||
CompositeCommand = require 'command-panel/commands/composite-command'
|
||||
PreviewList = require 'command-panel/preview-list'
|
||||
Editor = require 'editor'
|
||||
{SyntaxError} = require('pegjs').parser
|
||||
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class CommandPanel extends View
|
||||
@activate: (rootView, state) ->
|
||||
requireStylesheet 'command-panel.css'
|
||||
if state?
|
||||
@instance = CommandPanel.deserialize(state, rootView)
|
||||
else
|
||||
@instance = new CommandPanel(rootView)
|
||||
|
||||
@deactivate: ->
|
||||
@instance.destroy()
|
||||
|
||||
@serialize: ->
|
||||
text: @instance.miniEditor.getText()
|
||||
visible: @instance.hasParent()
|
||||
miniEditorFocused: @instance.miniEditor.isFocused
|
||||
|
||||
@deserialize: (state, rootView) ->
|
||||
commandPanel = new CommandPanel(rootView)
|
||||
commandPanel.attach(state.text, focus: false) if state.visible
|
||||
commandPanel.miniEditor.focus() if state.miniEditorFocused
|
||||
commandPanel
|
||||
|
||||
@content: (rootView) ->
|
||||
@div class: 'command-panel', =>
|
||||
@subview 'previewList', new PreviewList(rootView)
|
||||
@div class: 'prompt-and-editor', =>
|
||||
@div ':', class: 'prompt', outlet: 'prompt'
|
||||
@subview 'miniEditor', new Editor(mini: true)
|
||||
|
||||
commandInterpreter: null
|
||||
history: null
|
||||
historyIndex: 0
|
||||
|
||||
initialize: (@rootView)->
|
||||
@commandInterpreter = new CommandInterpreter(@rootView.project)
|
||||
@history = []
|
||||
|
||||
@on 'command-panel:unfocus', => @rootView.focus()
|
||||
@on 'command-panel:close', => @detach()
|
||||
|
||||
@rootView.on 'command-panel:toggle', => @toggle()
|
||||
@rootView.on 'command-panel:toggle-preview', => @togglePreview()
|
||||
@rootView.on 'command-panel:execute', => @execute()
|
||||
@rootView.on 'command-panel:find-in-file', => @attach("/")
|
||||
@rootView.on 'command-panel:find-in-project', => @attach("Xx/")
|
||||
@rootView.on 'command-panel:repeat-relative-address', => @repeatRelativeAddress()
|
||||
@rootView.on 'command-panel:repeat-relative-address-in-reverse', => @repeatRelativeAddressInReverse()
|
||||
@rootView.on 'command-panel:set-selection-as-regex-address', => @setSelectionAsLastRelativeAddress()
|
||||
|
||||
@miniEditor.off 'move-up move-down'
|
||||
@miniEditor.on 'move-up', => @navigateBackwardInHistory()
|
||||
@miniEditor.on 'move-down', => @navigateForwardInHistory()
|
||||
|
||||
@previewList.hide()
|
||||
|
||||
destroy: ->
|
||||
@previewList.destroy()
|
||||
|
||||
toggle: ->
|
||||
if @miniEditor.isFocused
|
||||
@detach()
|
||||
@rootView.focus()
|
||||
else
|
||||
@attach() unless @hasParent()
|
||||
@miniEditor.focus()
|
||||
|
||||
togglePreview: ->
|
||||
if @previewList.is(':focus')
|
||||
@previewList.hide()
|
||||
@detach()
|
||||
@rootView.focus()
|
||||
else
|
||||
@attach() unless @hasParent()
|
||||
if @previewList.hasOperations()
|
||||
@previewList.show().focus()
|
||||
else
|
||||
@miniEditor.focus()
|
||||
|
||||
attach: (text='', options={}) ->
|
||||
focus = options.focus ? true
|
||||
@rootView.vertical.append(this)
|
||||
@miniEditor.focus() if focus
|
||||
@miniEditor.setText(text)
|
||||
@miniEditor.setCursorBufferPosition([0, Infinity])
|
||||
|
||||
detach: ->
|
||||
@rootView.focus()
|
||||
@previewList.hide()
|
||||
super
|
||||
|
||||
execute: (command = @miniEditor.getText()) ->
|
||||
try
|
||||
@commandInterpreter.eval(command, @rootView.getActiveEditSession()).done (operationsToPreview) =>
|
||||
@history.push(command)
|
||||
@historyIndex = @history.length
|
||||
if operationsToPreview?.length
|
||||
@previewList.populate(operationsToPreview)
|
||||
@previewList.focus()
|
||||
else
|
||||
@detach()
|
||||
catch error
|
||||
if error.name is "SyntaxError"
|
||||
@flashError()
|
||||
return
|
||||
else
|
||||
throw error
|
||||
|
||||
navigateBackwardInHistory: ->
|
||||
return if @historyIndex == 0
|
||||
@historyIndex--
|
||||
@miniEditor.setText(@history[@historyIndex])
|
||||
|
||||
navigateForwardInHistory: ->
|
||||
return if @historyIndex == @history.length
|
||||
@historyIndex++
|
||||
@miniEditor.setText(@history[@historyIndex] or '')
|
||||
|
||||
repeatRelativeAddress: ->
|
||||
@commandInterpreter.repeatRelativeAddress(@rootView.getActiveEditSession())
|
||||
|
||||
repeatRelativeAddressInReverse: ->
|
||||
@commandInterpreter.repeatRelativeAddressInReverse(@rootView.getActiveEditSession())
|
||||
|
||||
setSelectionAsLastRelativeAddress: ->
|
||||
selection = @rootView.getActiveEditor().getSelectedText()
|
||||
regex = _.escapeRegExp(selection)
|
||||
@commandInterpreter.lastRelativeAddress = new CompositeCommand([new RegexAddress(regex)])
|
||||
58
src/extensions/command-panel/commands.pegjs
Normal file
58
src/extensions/command-panel/commands.pegjs
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
var CompositeCommand = require('command-panel/commands/composite-command')
|
||||
var Substitution = require('command-panel/commands/substitution');
|
||||
var ZeroAddress = require('command-panel/commands/zero-address');
|
||||
var EofAddress = require('command-panel/commands/eof-address');
|
||||
var LineAddress = require('command-panel/commands/line-address');
|
||||
var AddressRange = require('command-panel/commands/address-range');
|
||||
var CurrentSelectionAddress = require('command-panel/commands/current-selection-address')
|
||||
var RegexAddress = require('command-panel/commands/regex-address')
|
||||
var SelectAllMatches = require('command-panel/commands/select-all-matches')
|
||||
var SelectAllMatchesInProject = require('command-panel/commands/select-all-matches-in-project')
|
||||
}
|
||||
|
||||
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 ZeroAddress()
|
||||
if (!end) end = new EofAddress()
|
||||
return new AddressRange(start, end)
|
||||
}
|
||||
|
||||
primitiveAddress
|
||||
= '0' { return new ZeroAddress() }
|
||||
/ '$' { return new EofAddress() }
|
||||
/ '.' { return new CurrentSelectionAddress() }
|
||||
/ lineNumber:integer { return new LineAddress(lineNumber) }
|
||||
/ regexAddress
|
||||
|
||||
regexAddress
|
||||
= reverse:'-'? '/' pattern:pattern '/'? { return new RegexAddress(pattern, reverse.length > 0)}
|
||||
|
||||
command = substitution / selectAllMatches / selectAllMatchesInProject
|
||||
|
||||
substitution
|
||||
= "s" _ "/" find:pattern "/" replace:pattern "/" _ options:[g]* {
|
||||
return new Substitution(find, replace, options);
|
||||
}
|
||||
|
||||
selectAllMatches
|
||||
= 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatches(pattern) }
|
||||
|
||||
selectAllMatchesInProject
|
||||
= 'X' _ 'x' _ '/' pattern:pattern '/'? { return new SelectAllMatchesInProject(pattern) }
|
||||
|
||||
pattern
|
||||
= pattern:[^/]* { return pattern.join('') }
|
||||
|
||||
integer
|
||||
= digits:[0-9]+ { return parseInt(digits.join('')); }
|
||||
|
||||
_ = " "*
|
||||
12
src/extensions/command-panel/commands/address-range.coffee
Normal file
12
src/extensions/command-panel/commands/address-range.coffee
Normal file
@@ -0,0 +1,12 @@
|
||||
Address = require 'command-panel/commands/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class AddressRange extends Address
|
||||
constructor: (@startAddress, @endAddress) ->
|
||||
|
||||
getRange: (buffer, range) ->
|
||||
new Range(@startAddress.getRange(buffer, range).start, @endAddress.getRange(buffer, range).end)
|
||||
|
||||
isRelative: ->
|
||||
@startAddress.isRelative() and @endAddress.isRelative()
|
||||
17
src/extensions/command-panel/commands/address.coffee
Normal file
17
src/extensions/command-panel/commands/address.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
Command = require 'command-panel/commands/command'
|
||||
Operation = require 'command-panel/operation'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class Address extends Command
|
||||
compile: (project, buffer, ranges) ->
|
||||
deferred = $.Deferred()
|
||||
deferred.resolve ranges.map (range) =>
|
||||
new Operation
|
||||
project: project
|
||||
buffer: buffer
|
||||
bufferRange: @getRange(buffer, range)
|
||||
|
||||
deferred.promise()
|
||||
|
||||
isAddress: -> true
|
||||
7
src/extensions/command-panel/commands/command.coffee
Normal file
7
src/extensions/command-panel/commands/command.coffee
Normal file
@@ -0,0 +1,7 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Command
|
||||
isAddress: -> false
|
||||
preserveSelections: false
|
||||
previewOperations: false
|
||||
@@ -0,0 +1,43 @@
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class CompositeCommand
|
||||
constructor: (@subcommands) ->
|
||||
|
||||
execute: (project, editSession) ->
|
||||
currentRanges = editSession?.getSelectedBufferRanges()
|
||||
@executeCommands(@subcommands, project, editSession, currentRanges)
|
||||
|
||||
executeCommands: (commands, project, editSession, ranges) ->
|
||||
deferred = $.Deferred()
|
||||
[currentCommand, remainingCommands...] = commands
|
||||
|
||||
currentCommand.compile(project, editSession?.buffer, ranges).done (operations) =>
|
||||
if remainingCommands.length
|
||||
nextRanges = operations.map (operation) ->
|
||||
operation.destroy()
|
||||
operation.getBufferRange()
|
||||
@executeCommands(remainingCommands, project, editSession, nextRanges).done ->
|
||||
deferred.resolve()
|
||||
else
|
||||
if currentCommand.previewOperations
|
||||
deferred.resolve(operations)
|
||||
else
|
||||
bufferRanges = []
|
||||
for operation in operations
|
||||
bufferRange = operation.execute(editSession)
|
||||
bufferRanges.push(bufferRange) if bufferRange
|
||||
operation.destroy()
|
||||
if bufferRanges.length and not currentCommand.preserveSelections
|
||||
editSession.setSelectedBufferRanges(bufferRanges)
|
||||
deferred.resolve()
|
||||
|
||||
deferred.promise()
|
||||
|
||||
reverse: ->
|
||||
new CompositeCommand(@subcommands.map (command) -> command.reverse())
|
||||
|
||||
isRelativeAddress: ->
|
||||
_.all(@subcommands, (command) -> command.isAddress() and command.isRelative())
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Address = require 'command-panel/commands/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class CurrentSelectionAddress extends Address
|
||||
getRange: (buffer, range) ->
|
||||
range
|
||||
|
||||
isRelative: -> true
|
||||
10
src/extensions/command-panel/commands/eof-address.coffee
Normal file
10
src/extensions/command-panel/commands/eof-address.coffee
Normal file
@@ -0,0 +1,10 @@
|
||||
Address = require 'command-panel/commands/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class EofAddress extends Address
|
||||
getRange: (buffer, range) ->
|
||||
eof = buffer.getEofPosition()
|
||||
new Range(eof, eof)
|
||||
|
||||
isRelative: -> false
|
||||
12
src/extensions/command-panel/commands/line-address.coffee
Normal file
12
src/extensions/command-panel/commands/line-address.coffee
Normal file
@@ -0,0 +1,12 @@
|
||||
Address = require 'command-panel/commands/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
|
||||
36
src/extensions/command-panel/commands/regex-address.coffee
Normal file
36
src/extensions/command-panel/commands/regex-address.coffee
Normal file
@@ -0,0 +1,36 @@
|
||||
Address = require 'command-panel/commands/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class RegexAddress extends Address
|
||||
regex: null
|
||||
reverse: null
|
||||
|
||||
constructor: (pattern, isReversed) ->
|
||||
@isReversed = isReversed
|
||||
@regex = new RegExp(pattern)
|
||||
|
||||
getRange: (buffer, range) ->
|
||||
rangeBefore = new Range([0, 0], range.start)
|
||||
rangeAfter = new Range(range.end, buffer.getEofPosition())
|
||||
|
||||
rangeToSearch = if @isReversed then rangeBefore else rangeAfter
|
||||
|
||||
rangeToReturn = null
|
||||
scanMethodName = if @isReversed then "backwardsScanInRange" else "scanInRange"
|
||||
buffer[scanMethodName] @regex, rangeToSearch, (match, range) ->
|
||||
rangeToReturn = range
|
||||
|
||||
if rangeToReturn
|
||||
rangeToReturn
|
||||
else
|
||||
rangeToSearch = if @isReversed then rangeAfter else rangeBefore
|
||||
buffer[scanMethodName] @regex, rangeToSearch, (match, range) ->
|
||||
rangeToReturn = range
|
||||
|
||||
rangeToReturn or range
|
||||
|
||||
isRelative: -> true
|
||||
|
||||
reverse: ->
|
||||
new RegexAddress(@regex, !@isReversed)
|
||||
@@ -0,0 +1,24 @@
|
||||
Command = require 'command-panel/commands/command'
|
||||
Operation = require 'command-panel/operation'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class SelectAllMatchesInProject extends Command
|
||||
regex: null
|
||||
previewOperations: true
|
||||
|
||||
constructor: (pattern) ->
|
||||
@regex = new RegExp(pattern, 'g')
|
||||
|
||||
compile: (project, buffer, range) ->
|
||||
deferred = $.Deferred()
|
||||
operations = []
|
||||
promise = project.scan @regex, ({path, range}) ->
|
||||
operations.push(new Operation(
|
||||
project: project
|
||||
buffer: project.bufferForPath(path)
|
||||
bufferRange: range
|
||||
))
|
||||
|
||||
promise.done -> deferred.resolve(operations)
|
||||
deferred.promise()
|
||||
@@ -0,0 +1,23 @@
|
||||
Command = require 'command-panel/commands/command'
|
||||
Operation = require 'command-panel/operation'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class SelectAllMatches extends Command
|
||||
regex: null
|
||||
|
||||
constructor: (pattern) ->
|
||||
@regex = new RegExp(pattern, 'g')
|
||||
|
||||
compile: (project, buffer, ranges) ->
|
||||
deferred = $.Deferred()
|
||||
operations = []
|
||||
for range in ranges
|
||||
buffer.scanInRange @regex, range, (match, matchRange) ->
|
||||
operations.push(new Operation(
|
||||
project: project
|
||||
buffer: buffer
|
||||
bufferRange: matchRange
|
||||
))
|
||||
deferred.resolve(operations)
|
||||
deferred.promise()
|
||||
28
src/extensions/command-panel/commands/substitution.coffee
Normal file
28
src/extensions/command-panel/commands/substitution.coffee
Normal file
@@ -0,0 +1,28 @@
|
||||
Command = require 'command-panel/commands/command'
|
||||
Operation = require 'command-panel/operation'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class Substitution extends Command
|
||||
regex: null
|
||||
replacementText: null
|
||||
preserveSelections: true
|
||||
|
||||
constructor: (pattern, replacementText, options) ->
|
||||
@replacementText = replacementText
|
||||
@regex = new RegExp(pattern, options.join(''))
|
||||
|
||||
compile: (project, buffer, ranges) ->
|
||||
deferred = $.Deferred()
|
||||
operations = []
|
||||
for range in ranges
|
||||
buffer.scanInRange @regex, range, (match, matchRange, { replace }) =>
|
||||
operations.push(new Operation(
|
||||
project: project
|
||||
buffer: buffer
|
||||
bufferRange: matchRange
|
||||
newText: @replacementText
|
||||
preserveSelection: true
|
||||
))
|
||||
deferred.resolve(operations)
|
||||
deferred.promise()
|
||||
@@ -0,0 +1,9 @@
|
||||
Address = require 'command-panel/commands/address'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class ZeroAddress extends Address
|
||||
getRange: ->
|
||||
new Range([0, 0], [0, 0])
|
||||
|
||||
isRelative: -> false
|
||||
1
src/extensions/command-panel/index.coffee
Normal file
1
src/extensions/command-panel/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'command-panel/command-panel'
|
||||
17
src/extensions/command-panel/keymap.coffee
Normal file
17
src/extensions/command-panel/keymap.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'ctrl-0': 'command-panel:toggle'
|
||||
'ctrl-2': 'command-panel:toggle-preview'
|
||||
'meta-:': 'command-panel:toggle'
|
||||
'meta-F': 'command-panel:find-in-project'
|
||||
|
||||
window.keymap.bindKeys '.command-panel .preview-list, .command-panel .editor input',
|
||||
'meta-w': 'command-panel:close'
|
||||
escape: 'command-panel:unfocus'
|
||||
enter: 'command-panel:execute'
|
||||
|
||||
window.keymap.bindKeys '.editor',
|
||||
'meta-g': 'command-panel:repeat-relative-address'
|
||||
'meta-G': 'command-panel:repeat-relative-address-in-reverse'
|
||||
'meta-e': 'command-panel:set-selection-as-regex-address'
|
||||
'meta-f': 'command-panel:find-in-file'
|
||||
'meta-F': 'command-panel:find-in-project'
|
||||
30
src/extensions/command-panel/operation.coffee
Normal file
30
src/extensions/command-panel/operation.coffee
Normal file
@@ -0,0 +1,30 @@
|
||||
{$$$} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class Operation
|
||||
constructor: ({@project, @buffer, bufferRange, @newText, @preserveSelection}) ->
|
||||
@buffer.retain()
|
||||
@anchorRange = @buffer.addAnchorRange(bufferRange)
|
||||
|
||||
getPath: ->
|
||||
@project.relativize(@buffer.getPath())
|
||||
|
||||
getBufferRange: ->
|
||||
@anchorRange.getBufferRange()
|
||||
|
||||
execute: (editSession) ->
|
||||
@buffer.change(@getBufferRange(), @newText) if @newText
|
||||
@getBufferRange() unless @preserveSelection
|
||||
|
||||
preview: ->
|
||||
range = @anchorRange.getBufferRange()
|
||||
line = @buffer.lineForRow(range.start.row)
|
||||
prefix = line[0...range.start.column]
|
||||
match = line[range.start.column...range.end.column]
|
||||
suffix = line[range.end.column..]
|
||||
|
||||
{prefix, suffix, match}
|
||||
|
||||
destroy: ->
|
||||
@buffer.release()
|
||||
@anchorRange.destroy()
|
||||
83
src/extensions/command-panel/preview-list.coffee
Normal file
83
src/extensions/command-panel/preview-list.coffee
Normal file
@@ -0,0 +1,83 @@
|
||||
$ = require 'jquery'
|
||||
{$$$, View} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class PreviewList extends View
|
||||
@content: ->
|
||||
@ol class: 'preview-list', tabindex: -1, ->
|
||||
|
||||
selectedOperationIndex: 0
|
||||
operations: null
|
||||
|
||||
initialize: (@rootView) ->
|
||||
@on 'move-down', => @selectNextOperation()
|
||||
@on 'move-up', => @selectPreviousOperation()
|
||||
@on 'command-panel:execute', => @executeSelectedOperation()
|
||||
|
||||
@on 'mousedown', 'li', (e) =>
|
||||
@setSelectedOperationIndex(parseInt($(e.target).closest('li').data('index')))
|
||||
@executeSelectedOperation()
|
||||
|
||||
destroy: ->
|
||||
@destroyOperations() if @operations
|
||||
|
||||
hasOperations: -> @operations?
|
||||
|
||||
populate: (operations) ->
|
||||
@destroyOperations() if @operations
|
||||
@operations = operations
|
||||
@empty()
|
||||
@html $$$ ->
|
||||
for operation, index in operations
|
||||
{prefix, suffix, match} = operation.preview()
|
||||
@li 'data-index': index, =>
|
||||
@span operation.getPath(), outlet: "path", class: "path"
|
||||
@span outlet: "preview", class: "preview", =>
|
||||
@span prefix
|
||||
@span match, class: 'match'
|
||||
@span suffix
|
||||
|
||||
@setSelectedOperationIndex(0)
|
||||
@show()
|
||||
|
||||
selectNextOperation: ->
|
||||
@setSelectedOperationIndex(@selectedOperationIndex + 1)
|
||||
|
||||
selectPreviousOperation: ->
|
||||
@setSelectedOperationIndex(@selectedOperationIndex - 1)
|
||||
|
||||
setSelectedOperationIndex: (index) ->
|
||||
index = Math.max(0, index)
|
||||
index = Math.min(@operations.length - 1, index)
|
||||
@children(".selected").removeClass('selected')
|
||||
element = @children("li:eq(#{index})")
|
||||
element.addClass('selected')
|
||||
@scrollToElement(element)
|
||||
@selectedOperationIndex = index
|
||||
|
||||
executeSelectedOperation: ->
|
||||
operation = @getSelectedOperation()
|
||||
editSession = @rootView.open(operation.getPath())
|
||||
bufferRange = operation.execute(editSession)
|
||||
editSession.setSelectedBufferRange(bufferRange) if bufferRange
|
||||
@rootView.focus()
|
||||
false
|
||||
|
||||
getOperations: ->
|
||||
new Array(@operations...)
|
||||
|
||||
destroyOperations: ->
|
||||
operation.destroy() for operation in @getOperations()
|
||||
@operations = null
|
||||
|
||||
getSelectedOperation: ->
|
||||
@operations[@selectedOperationIndex]
|
||||
|
||||
scrollToElement: (element) ->
|
||||
top = @scrollTop() + element.position().top
|
||||
bottom = top + element.outerHeight()
|
||||
|
||||
if bottom > @scrollBottom()
|
||||
@scrollBottom(bottom)
|
||||
if top < @scrollTop()
|
||||
@scrollTop(top)
|
||||
110
src/extensions/fuzzy-finder/fuzzy-finder.coffee
Normal file
110
src/extensions/fuzzy-finder/fuzzy-finder.coffee
Normal file
@@ -0,0 +1,110 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
stringScore = require 'stringscore'
|
||||
fuzzyFilter = require 'fuzzy-filter'
|
||||
$ = require 'jquery'
|
||||
Editor = require 'editor'
|
||||
|
||||
module.exports =
|
||||
class FuzzyFinder extends View
|
||||
@activate: (rootView) ->
|
||||
@instance = new FuzzyFinder(rootView)
|
||||
|
||||
@content: ->
|
||||
@div class: 'fuzzy-finder', =>
|
||||
@ol outlet: 'pathList'
|
||||
@subview 'miniEditor', new Editor(mini: true)
|
||||
|
||||
paths: null
|
||||
allowActiveEditorChange: null
|
||||
maxResults: null
|
||||
|
||||
initialize: (@rootView) ->
|
||||
requireStylesheet 'fuzzy-finder.css'
|
||||
@maxResults = 10
|
||||
|
||||
@rootView.on 'fuzzy-finder:toggle-file-finder', => @toggleFileFinder()
|
||||
@rootView.on 'fuzzy-finder:toggle-buffer-finder', => @toggleBufferFinder()
|
||||
|
||||
@on 'fuzzy-finder:cancel', => @detach()
|
||||
@on 'move-up', => @moveUp()
|
||||
@on 'move-down', => @moveDown()
|
||||
@on 'fuzzy-finder:select-path', => @select()
|
||||
@on 'mousedown', 'li', (e) => @entryClicked(e)
|
||||
|
||||
@miniEditor.getBuffer().on 'change', => @populatePathList() if @hasParent()
|
||||
@miniEditor.off 'move-up move-down'
|
||||
|
||||
toggleFileFinder: ->
|
||||
if @hasParent()
|
||||
@detach()
|
||||
else
|
||||
return unless @rootView.project.getPath()?
|
||||
@allowActiveEditorChange = false
|
||||
@populateProjectPaths()
|
||||
@attach()
|
||||
|
||||
toggleBufferFinder: ->
|
||||
if @hasParent()
|
||||
@detach()
|
||||
else
|
||||
@allowActiveEditorChange = true
|
||||
@populateOpenBufferPaths()
|
||||
@attach() if @paths?.length
|
||||
|
||||
populateProjectPaths: ->
|
||||
@rootView.project.getFilePaths().done (@paths) => @populatePathList()
|
||||
|
||||
populateOpenBufferPaths: ->
|
||||
@paths = @rootView.getOpenBufferPaths().map (path) =>
|
||||
@rootView.project.relativize(path)
|
||||
@populatePathList() if @paths?.length
|
||||
|
||||
attach: ->
|
||||
@rootView.append(this)
|
||||
@miniEditor.focus()
|
||||
@miniEditor.on 'focusout', => @detach()
|
||||
|
||||
detach: ->
|
||||
@miniEditor.off 'focusout'
|
||||
@rootView.focus()
|
||||
super
|
||||
@miniEditor.setText('')
|
||||
|
||||
populatePathList: ->
|
||||
@pathList.empty()
|
||||
for path in @findMatches(@miniEditor.getText())
|
||||
@pathList.append $$ -> @li path
|
||||
|
||||
@pathList.children('li:first').addClass 'selected'
|
||||
|
||||
findSelectedLi: ->
|
||||
@pathList.children('li.selected')
|
||||
|
||||
open : (path) ->
|
||||
return unless path.length
|
||||
@rootView.open(path, {@allowActiveEditorChange})
|
||||
@detach()
|
||||
|
||||
select: ->
|
||||
@open(@findSelectedLi().text())
|
||||
|
||||
entryClicked: (e) ->
|
||||
@open($(e.currentTarget).text())
|
||||
false
|
||||
|
||||
moveUp: ->
|
||||
@findSelectedLi()
|
||||
.filter(':not(:first-child)')
|
||||
.removeClass('selected')
|
||||
.prev()
|
||||
.addClass('selected')
|
||||
|
||||
moveDown: ->
|
||||
@findSelectedLi()
|
||||
.filter(':not(:last-child)')
|
||||
.removeClass('selected')
|
||||
.next()
|
||||
.addClass('selected')
|
||||
|
||||
findMatches: (query) ->
|
||||
fuzzyFilter(@paths, query, maxResults: @maxResults)
|
||||
1
src/extensions/fuzzy-finder/index.coffee
Normal file
1
src/extensions/fuzzy-finder/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'fuzzy-finder/fuzzy-finder'
|
||||
8
src/extensions/fuzzy-finder/keymap.coffee
Normal file
8
src/extensions/fuzzy-finder/keymap.coffee
Normal file
@@ -0,0 +1,8 @@
|
||||
window.keymap.bindKeys '*'
|
||||
'meta-t': 'fuzzy-finder:toggle-file-finder'
|
||||
'meta-b': 'fuzzy-finder:toggle-buffer-finder'
|
||||
|
||||
window.keymap.bindKeys ".fuzzy-finder .editor input",
|
||||
'enter': 'fuzzy-finder:select-path',
|
||||
'escape': 'fuzzy-finder:cancel'
|
||||
'meta-w': 'fuzzy-finder:cancel'
|
||||
1
src/extensions/keybindings-view/index.coffee
Normal file
1
src/extensions/keybindings-view/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'keybindings-view/keybindings-view'
|
||||
31
src/extensions/keybindings-view/keybindings-view.coffee
Normal file
31
src/extensions/keybindings-view/keybindings-view.coffee
Normal file
@@ -0,0 +1,31 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
|
||||
module.exports =
|
||||
class KeybindingsView extends View
|
||||
@activate: (rootView, state) ->
|
||||
requireStylesheet 'keybinding-view.css'
|
||||
@instance = new this(rootView)
|
||||
|
||||
@content: (rootView) ->
|
||||
@div class: 'keybindings-view', tabindex: -1, =>
|
||||
@ul outlet: 'keybindingList'
|
||||
|
||||
initialize: (@rootView) ->
|
||||
@rootView.on 'keybindings-view:attach', => @attach()
|
||||
@on 'keybindings-view:detach', => @detach()
|
||||
|
||||
attach: ->
|
||||
@keybindingList.empty()
|
||||
@keybindingList.append $$ ->
|
||||
for keystroke, command of rootView.activeKeybindings()
|
||||
@li =>
|
||||
@span class: 'keystroke', "#{keystroke}"
|
||||
@span ":"
|
||||
@span "#{command}"
|
||||
|
||||
@rootView.append(this)
|
||||
@focus()
|
||||
|
||||
detach: ->
|
||||
super()
|
||||
@rootView.focus()
|
||||
5
src/extensions/keybindings-view/keymap.coffee
Normal file
5
src/extensions/keybindings-view/keymap.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
window.keymap.bindKeys '*',
|
||||
'ctrl-?': 'keybindings-view:attach'
|
||||
|
||||
window.keymap.bindKeys ".keybindings-view",
|
||||
'escape': 'keybindings-view:detach'
|
||||
1
src/extensions/snippets/index.coffee
Normal file
1
src/extensions/snippets/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'extensions/snippets/snippets.coffee'
|
||||
6
src/extensions/snippets/keymap.coffee
Normal file
6
src/extensions/snippets/keymap.coffee
Normal file
@@ -0,0 +1,6 @@
|
||||
window.keymap.bindKeys '.editor'
|
||||
'tab': 'snippets:expand'
|
||||
|
||||
window.keymap.bindKeys '.editor'
|
||||
'tab': 'snippets:next-tab-stop'
|
||||
'shift-tab': 'snippets:previous-tab-stop'
|
||||
61
src/extensions/snippets/snippet-expansion.coffee
Normal file
61
src/extensions/snippets/snippet-expansion.coffee
Normal file
@@ -0,0 +1,61 @@
|
||||
module.exports =
|
||||
class SnippetExpansion
|
||||
tabStopAnchorRanges: null
|
||||
|
||||
constructor: (snippet, @editSession) ->
|
||||
@editSession.selectToBeginningOfWord()
|
||||
startPosition = @editSession.getCursorBufferPosition()
|
||||
@editSession.transact =>
|
||||
@editSession.insertText(snippet.body, autoIndent: false)
|
||||
if snippet.tabStops.length
|
||||
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
|
||||
if snippet.lineCount > 1
|
||||
@indentSubsequentLines(startPosition.row, snippet)
|
||||
|
||||
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
|
||||
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
|
||||
@editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
|
||||
@setTabStopIndex(0)
|
||||
|
||||
indentSubsequentLines: (startRow, snippet) ->
|
||||
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
|
||||
for row in [startRow + 1...startRow + snippet.lineCount]
|
||||
@editSession.buffer.insert([row, 0], initialIndent)
|
||||
|
||||
goToNextTabStop: ->
|
||||
nextIndex = @tabStopIndex + 1
|
||||
if @cursorIsInsideTabStops() and nextIndex < @tabStopAnchorRanges.length
|
||||
@setTabStopIndex(nextIndex)
|
||||
true
|
||||
else
|
||||
@destroy()
|
||||
false
|
||||
|
||||
goToPreviousTabStop: ->
|
||||
if @cursorIsInsideTabStops()
|
||||
@setTabStopIndex(@tabStopIndex - 1) if @tabStopIndex > 0
|
||||
true
|
||||
else
|
||||
@destroy()
|
||||
false
|
||||
|
||||
ensureValidTabStops: ->
|
||||
@tabStopAnchorRanges? and @destroyIfCursorIsOutsideTabStops()
|
||||
|
||||
setTabStopIndex: (@tabStopIndex) ->
|
||||
@editSession.setSelectedBufferRange(@tabStopAnchorRanges[@tabStopIndex].getBufferRange())
|
||||
|
||||
cursorIsInsideTabStops: ->
|
||||
position = @editSession.getCursorBufferPosition()
|
||||
for anchorRange in @tabStopAnchorRanges
|
||||
return true if anchorRange.containsBufferPosition(position)
|
||||
false
|
||||
|
||||
destroy: ->
|
||||
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
|
||||
@editSession.snippetExpansion = null
|
||||
|
||||
restore: (@editSession) ->
|
||||
@editSession.snippetExpansion = this
|
||||
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
|
||||
@editSession.addAnchorRange(anchorRange.getBufferRange())
|
||||
36
src/extensions/snippets/snippet.coffee
Normal file
36
src/extensions/snippets/snippet.coffee
Normal file
@@ -0,0 +1,36 @@
|
||||
_ = require 'underscore'
|
||||
Range = require 'range'
|
||||
|
||||
module.exports =
|
||||
class Snippet
|
||||
body: null
|
||||
lineCount: null
|
||||
tabStops: null
|
||||
|
||||
constructor: ({@bodyPosition, @prefix, @description, body}) ->
|
||||
@body = @extractTabStops(body)
|
||||
|
||||
extractTabStops: (bodyLines) ->
|
||||
tabStopsByIndex = {}
|
||||
bodyText = []
|
||||
|
||||
[row, column] = [0, 0]
|
||||
for bodyLine, i in bodyLines
|
||||
lineText = []
|
||||
for segment in bodyLine
|
||||
if segment.index
|
||||
{ index, placeholderText } = segment
|
||||
tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length])
|
||||
lineText.push(placeholderText)
|
||||
else
|
||||
lineText.push(segment)
|
||||
column += segment.length
|
||||
bodyText.push(lineText.join(''))
|
||||
row++; column = 0
|
||||
@lineCount = row
|
||||
|
||||
@tabStops = []
|
||||
for index in _.keys(tabStopsByIndex).sort()
|
||||
@tabStops.push tabStopsByIndex[index]
|
||||
|
||||
bodyText.join('\n')
|
||||
46
src/extensions/snippets/snippets.coffee
Normal file
46
src/extensions/snippets/snippets.coffee
Normal file
@@ -0,0 +1,46 @@
|
||||
fs = require 'fs'
|
||||
PEG = require 'pegjs'
|
||||
_ = require 'underscore'
|
||||
SnippetExpansion = require 'snippets/snippet-expansion'
|
||||
|
||||
module.exports =
|
||||
name: 'Snippets'
|
||||
snippetsByExtension: {}
|
||||
snippetsParser: PEG.buildParser(fs.read(require.resolve 'extensions/snippets/snippets.pegjs'), trackLineAndColumn: true)
|
||||
|
||||
activate: (@rootView) ->
|
||||
@loadSnippets()
|
||||
@rootView.on 'editor-open', (e, editor) => @enableSnippetsInEditor(editor)
|
||||
|
||||
loadSnippets: ->
|
||||
snippetsDir = fs.join(atom.configDirPath, 'snippets')
|
||||
if fs.exists(snippetsDir)
|
||||
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
|
||||
|
||||
loadSnippetsFile: (path) ->
|
||||
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
|
||||
|
||||
evalSnippets: (extension, text) ->
|
||||
@snippetsByExtension[extension] = @snippetsParser.parse(text)
|
||||
|
||||
enableSnippetsInEditor: (editor) ->
|
||||
editor.on 'snippets:expand', (e) =>
|
||||
editSession = editor.activeEditSession
|
||||
prefix = editSession.getLastCursor().getCurrentWordPrefix()
|
||||
if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
|
||||
editSession.transact ->
|
||||
snippetExpansion = new SnippetExpansion(snippet, editSession)
|
||||
editSession.snippetExpansion = snippetExpansion
|
||||
editSession.pushOperation
|
||||
undo: -> snippetExpansion.destroy()
|
||||
redo: (editSession) -> snippetExpansion.restore(editSession)
|
||||
else
|
||||
e.abortKeyBinding()
|
||||
|
||||
editor.on 'snippets:next-tab-stop', (e) ->
|
||||
unless editor.activeEditSession.snippetExpansion?.goToNextTabStop()
|
||||
e.abortKeyBinding()
|
||||
|
||||
editor.on 'snippets:previous-tab-stop', (e) ->
|
||||
unless editor.activeEditSession.snippetExpansion?.goToPreviousTabStop()
|
||||
e.abortKeyBinding()
|
||||
39
src/extensions/snippets/snippets.pegjs
Normal file
39
src/extensions/snippets/snippets.pegjs
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
var Snippet = require('extensions/snippets/snippet');
|
||||
var Point = require('point');
|
||||
}
|
||||
|
||||
snippets = snippets:snippet+ ws? {
|
||||
var snippetsByPrefix = {};
|
||||
snippets.forEach(function(snippet) {
|
||||
snippetsByPrefix[snippet.prefix] = snippet
|
||||
});
|
||||
return snippetsByPrefix;
|
||||
}
|
||||
|
||||
snippet = ws? start ws prefix:prefix ws description:string bodyPosition:beforeBody body:body end {
|
||||
return new Snippet({ bodyPosition: bodyPosition, prefix: prefix, description: description, body: body });
|
||||
}
|
||||
|
||||
start = 'snippet'
|
||||
prefix = prefix:[A-Za-z0-9_]+ { return prefix.join(''); }
|
||||
string = ['] body:[^']* ['] { return body.join(''); }
|
||||
/ ["] body:[^"]* ["] { return body.join(''); }
|
||||
|
||||
beforeBody = [ ]* '\n' { return new Point(line, 0); } // return start position of body: body begins on next line, so don't subtract 1 from line
|
||||
|
||||
body = bodyLine+
|
||||
bodyLine = content:(tabStop / bodyText)* '\n' { return content; }
|
||||
bodyText = text:bodyChar+ { return text.join(''); }
|
||||
bodyChar = !(end / tabStop) char:[^\n] { return char; }
|
||||
tabStop = simpleTabStop / tabStopWithPlaceholder
|
||||
simpleTabStop = '$' index:[0-9]+ {
|
||||
return { index: parseInt(index), placeholderText: '' };
|
||||
}
|
||||
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
|
||||
return { index: parseInt(index), placeholderText: placeholderText.join('') };
|
||||
}
|
||||
|
||||
end = 'endsnippet'
|
||||
ws = ([ \n] / comment)+
|
||||
comment = '#' [^\n]*
|
||||
14
src/extensions/strip-trailing-whitespace.coffee
Normal file
14
src/extensions/strip-trailing-whitespace.coffee
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports =
|
||||
name: "strip trailing whitespace"
|
||||
|
||||
activate: (rootView) ->
|
||||
for buffer in rootView.project.getBuffers()
|
||||
@stripTrailingWhitespaceBeforeSave(buffer)
|
||||
|
||||
rootView.project.on 'new-buffer', (buffer) =>
|
||||
@stripTrailingWhitespaceBeforeSave(buffer)
|
||||
|
||||
stripTrailingWhitespaceBeforeSave: (buffer) ->
|
||||
buffer.on 'before-save', ->
|
||||
buffer.scan /[ \t]+$/g, (match, range, { replace }) ->
|
||||
replace('')
|
||||
38
src/extensions/tree-view/dialog.coffee
Normal file
38
src/extensions/tree-view/dialog.coffee
Normal file
@@ -0,0 +1,38 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
Editor = require 'editor'
|
||||
fs = require 'fs'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class Dialog extends View
|
||||
@content: ({prompt} = {}) ->
|
||||
@div class: 'tree-view-dialog', =>
|
||||
@div prompt, outlet: 'prompt'
|
||||
@subview 'miniEditor', new Editor(mini: true)
|
||||
|
||||
initialize: ({path, @onConfirm, select} = {}) ->
|
||||
@miniEditor.focus()
|
||||
@on 'tree-view:confirm', => @confirm()
|
||||
@on 'tree-view:cancel', => @cancel()
|
||||
@miniEditor.on 'focusout', => @remove()
|
||||
|
||||
@miniEditor.setText(path)
|
||||
|
||||
if select
|
||||
extension = fs.extension(path)
|
||||
baseName = fs.base(path)
|
||||
range = [[0, path.length - baseName.length], [0, path.length - extension.length]]
|
||||
@miniEditor.setSelectedBufferRange(range)
|
||||
|
||||
confirm: ->
|
||||
return if @onConfirm(@miniEditor.getText()) is false
|
||||
@remove()
|
||||
$('#root-view').focus()
|
||||
|
||||
cancel: ->
|
||||
@remove()
|
||||
$('.tree-view').focus()
|
||||
|
||||
showError: (message) ->
|
||||
@prompt.text(message)
|
||||
@prompt.flashError()
|
||||
84
src/extensions/tree-view/directory-view.coffee
Normal file
84
src/extensions/tree-view/directory-view.coffee
Normal file
@@ -0,0 +1,84 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
FileView = require 'tree-view/file-view'
|
||||
Directory = require 'directory'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class DirectoryView extends View
|
||||
@content: ({directory, isExpanded} = {}) ->
|
||||
@li class: 'directory entry', =>
|
||||
@div outlet: 'header', class: 'header', =>
|
||||
@span '▸', class: 'disclosure-arrow', outlet: 'disclosureArrow'
|
||||
@span directory.getBaseName(), class: 'name'
|
||||
|
||||
directory: null
|
||||
entries: null
|
||||
header: null
|
||||
|
||||
initialize: ({@directory, isExpanded} = {}) ->
|
||||
@expand() if isExpanded
|
||||
@disclosureArrow.on 'click', => @toggleExpansion()
|
||||
|
||||
getPath: ->
|
||||
@directory.path
|
||||
|
||||
buildEntries: ->
|
||||
@unwatchDescendantEntries()
|
||||
@entries?.remove()
|
||||
@entries = $$ -> @ol class: 'entries'
|
||||
for entry in @directory.getEntries()
|
||||
if entry instanceof Directory
|
||||
@entries.append(new DirectoryView(directory: entry, isExpanded: false))
|
||||
else
|
||||
@entries.append(new FileView(entry))
|
||||
@append(@entries)
|
||||
|
||||
toggleExpansion: ->
|
||||
if @isExpanded then @collapse() else @expand()
|
||||
|
||||
expand: ->
|
||||
return if @isExpanded
|
||||
@addClass('expanded')
|
||||
@disclosureArrow.text('▾')
|
||||
@buildEntries()
|
||||
@watchEntries()
|
||||
@deserializeEntryExpansionStates(@entryStates) if @entryStates?
|
||||
@isExpanded = true
|
||||
false
|
||||
|
||||
collapse: ->
|
||||
@entryStates = @serializeEntryExpansionStates()
|
||||
@removeClass('expanded')
|
||||
@disclosureArrow.text('▸')
|
||||
@unwatchEntries()
|
||||
@entries.remove()
|
||||
@entries = null
|
||||
@isExpanded = false
|
||||
|
||||
watchEntries: ->
|
||||
@directory.on "contents-change.#{@directory.path}", =>
|
||||
@buildEntries()
|
||||
@trigger "tree-view:directory-modified"
|
||||
|
||||
unwatchEntries: ->
|
||||
@unwatchDescendantEntries()
|
||||
@directory.off ".#{@directory.path}"
|
||||
|
||||
unwatchDescendantEntries: ->
|
||||
@find('.expanded.directory').each ->
|
||||
$(this).view().unwatchEntries()
|
||||
|
||||
serializeEntryExpansionStates: ->
|
||||
entryStates = {}
|
||||
@entries?.find('> .directory.expanded').each ->
|
||||
view = $(this).view()
|
||||
entryStates[view.directory.getBaseName()] = view.serializeEntryExpansionStates()
|
||||
entryStates
|
||||
|
||||
deserializeEntryExpansionStates: (entryStates) ->
|
||||
for directoryName, childEntryStates of entryStates
|
||||
@entries.find("> .directory:contains('#{directoryName}')").each ->
|
||||
view = $(this).view()
|
||||
view.entryStates = childEntryStates
|
||||
view.expand()
|
||||
|
||||
14
src/extensions/tree-view/file-view.coffee
Normal file
14
src/extensions/tree-view/file-view.coffee
Normal file
@@ -0,0 +1,14 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
class FileView extends View
|
||||
@content: (file) ->
|
||||
@li file.getBaseName(), class: 'file entry'
|
||||
|
||||
file: null
|
||||
|
||||
initialize: (@file) ->
|
||||
|
||||
getPath: ->
|
||||
@file.path
|
||||
1
src/extensions/tree-view/index.coffee
Normal file
1
src/extensions/tree-view/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require 'tree-view/tree-view'
|
||||
18
src/extensions/tree-view/keymap.coffee
Normal file
18
src/extensions/tree-view/keymap.coffee
Normal file
@@ -0,0 +1,18 @@
|
||||
window.keymap.bindKeys '#root-view'
|
||||
'ctrl-1': 'tree-view:toggle'
|
||||
'ctrl-meta-1': 'tree-view:reveal-active-file'
|
||||
|
||||
window.keymap.bindKeys '.tree-view'
|
||||
'escape': 'tree-view:unfocus'
|
||||
'meta-w': 'tree-view:toggle'
|
||||
'right': 'tree-view:expand-directory'
|
||||
'left': 'tree-view:collapse-directory'
|
||||
'enter': 'tree-view:open-selected-entry'
|
||||
'm': 'tree-view:move'
|
||||
'a': 'tree-view:add'
|
||||
'delete': 'tree-view:remove'
|
||||
'backspace': 'tree-view:remove'
|
||||
|
||||
window.keymap.bindKeys '.tree-view-dialog .mini.editor'
|
||||
'enter': 'tree-view:confirm'
|
||||
'escape': 'tree-view:cancel'
|
||||
265
src/extensions/tree-view/tree-view.coffee
Normal file
265
src/extensions/tree-view/tree-view.coffee
Normal file
@@ -0,0 +1,265 @@
|
||||
{View, $$} = require 'space-pen'
|
||||
Directory = require 'directory'
|
||||
DirectoryView = require 'tree-view/directory-view'
|
||||
FileView = require 'tree-view/file-view'
|
||||
Dialog = require 'tree-view/dialog'
|
||||
Native = require 'native'
|
||||
fs = require 'fs'
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class TreeView extends View
|
||||
@activate: (rootView, state) ->
|
||||
requireStylesheet 'tree-view.css'
|
||||
|
||||
if state
|
||||
@instance = TreeView.deserialize(state, rootView)
|
||||
else
|
||||
@instance = new TreeView(rootView)
|
||||
@instance.attach()
|
||||
|
||||
@deactivate: () ->
|
||||
@instance.deactivate()
|
||||
|
||||
@serialize: ->
|
||||
@instance.serialize()
|
||||
|
||||
@content: (rootView) ->
|
||||
@div class: 'tree-view', tabindex: -1, =>
|
||||
if rootView.project.getRootDirectory()
|
||||
@subview 'root', new DirectoryView(directory: rootView.project.getRootDirectory(), isExpanded: true)
|
||||
|
||||
@deserialize: (state, rootView) ->
|
||||
treeView = new TreeView(rootView)
|
||||
treeView.root.deserializeEntryExpansionStates(state.directoryExpansionStates)
|
||||
treeView.selectEntryForPath(state.selectedPath)
|
||||
treeView.focusAfterAttach = state.hasFocus
|
||||
treeView.attach() if state.attached
|
||||
treeView
|
||||
|
||||
root: null
|
||||
focusAfterAttach: false
|
||||
|
||||
initialize: (@rootView) ->
|
||||
@on 'click', '.entry', (e) => @entryClicked(e)
|
||||
@on 'move-up', => @moveUp()
|
||||
@on 'move-down', => @moveDown()
|
||||
@on 'tree-view:expand-directory', => @expandDirectory()
|
||||
@on 'tree-view:collapse-directory', => @collapseDirectory()
|
||||
@on 'tree-view:open-selected-entry', => @openSelectedEntry()
|
||||
@on 'tree-view:move', => @moveSelectedEntry()
|
||||
@on 'tree-view:add', => @add()
|
||||
@on 'tree-view:remove', => @removeSelectedEntry()
|
||||
@on 'tree-view:directory-modified', => @selectActiveFile()
|
||||
@on 'tree-view:unfocus', => @rootView.focus()
|
||||
@rootView.on 'tree-view:toggle', => @toggle()
|
||||
@rootView.on 'tree-view:reveal-active-file', => @revealActiveFile()
|
||||
@rootView.on 'active-editor-path-change', => @selectActiveFile()
|
||||
@rootView.project.on 'path-change', => @updateRoot()
|
||||
|
||||
@selectEntry(@root) if @root
|
||||
|
||||
afterAttach: (onDom) ->
|
||||
@focus() if @focusAfterAttach
|
||||
|
||||
serialize: ->
|
||||
directoryExpansionStates: @root?.serializeEntryExpansionStates()
|
||||
selectedPath: @selectedEntry()?.getPath()
|
||||
hasFocus: @is(':focus')
|
||||
attached: @hasParent()
|
||||
|
||||
deactivate: ->
|
||||
@root?.unwatchEntries()
|
||||
|
||||
toggle: ->
|
||||
if @is(':focus')
|
||||
@detach()
|
||||
@rootView.focus()
|
||||
else
|
||||
@attach() unless @hasParent()
|
||||
@focus()
|
||||
|
||||
attach: ->
|
||||
@rootView.horizontal.prepend(this)
|
||||
|
||||
entryClicked: (e) ->
|
||||
entry = $(e.currentTarget).view()
|
||||
switch e.originalEvent?.detail ? 1
|
||||
when 1
|
||||
@selectEntry(entry)
|
||||
@openSelectedEntry() if (entry instanceof FileView)
|
||||
when 2
|
||||
if entry.is('.selected.file')
|
||||
@rootView.getActiveEditor().focus()
|
||||
else if entry.is('.selected.directory')
|
||||
entry.toggleExpansion()
|
||||
|
||||
false
|
||||
|
||||
updateRoot: ->
|
||||
@root?.remove()
|
||||
@root = new DirectoryView(directory: @rootView.project.getRootDirectory(), isExpanded: true)
|
||||
@append(@root)
|
||||
|
||||
selectActiveFile: ->
|
||||
activeFilePath = @rootView.getActiveEditor()?.getPath()
|
||||
@selectEntryForPath(activeFilePath)
|
||||
|
||||
revealActiveFile: ->
|
||||
@attach()
|
||||
@focus()
|
||||
|
||||
return unless activeFilePath = @rootView.getActiveEditor()?.getPath()
|
||||
|
||||
project = @rootView.project
|
||||
activePathComponents = project.relativize(activeFilePath).split('/')
|
||||
currentPath = project.getPath().replace(/\/$/, '')
|
||||
for pathComponent in activePathComponents
|
||||
currentPath += '/' + pathComponent
|
||||
entry = @entryForPath(currentPath)
|
||||
if entry.hasClass('directory')
|
||||
entry.expand()
|
||||
else
|
||||
@selectEntry(entry)
|
||||
|
||||
entryForPath: (path) ->
|
||||
fn = (bestMatchEntry, element) ->
|
||||
entry = $(element).view()
|
||||
regex = new RegExp("^" + _.escapeRegExp(entry.getPath()))
|
||||
if regex.test(path) and entry.getPath().length > bestMatchEntry.getPath().length
|
||||
entry
|
||||
else
|
||||
bestMatchEntry
|
||||
|
||||
@find(".entry").toArray().reduce(fn, @root)
|
||||
|
||||
selectEntryForPath: (path) ->
|
||||
@selectEntry(@entryForPath(path))
|
||||
|
||||
moveDown: ->
|
||||
selectedEntry = @selectedEntry()
|
||||
if selectedEntry
|
||||
if selectedEntry.is('.expanded.directory')
|
||||
return if @selectEntry(selectedEntry.find('.entry:first'))
|
||||
until @selectEntry(selectedEntry.next())
|
||||
selectedEntry = selectedEntry.parents('.entry:first')
|
||||
break unless selectedEntry.length
|
||||
else
|
||||
@selectEntry(@root)
|
||||
|
||||
@scrollToEntry(@selectedEntry())
|
||||
|
||||
moveUp: ->
|
||||
selectedEntry = @selectedEntry()
|
||||
if selectedEntry
|
||||
if previousEntry = @selectEntry(selectedEntry.prev())
|
||||
if previousEntry.is('.expanded.directory')
|
||||
@selectEntry(previousEntry.find('.entry:last'))
|
||||
else
|
||||
@selectEntry(selectedEntry.parents('.directory').first())
|
||||
else
|
||||
@selectEntry(@find('.entry').last())
|
||||
|
||||
@scrollToEntry(@selectedEntry())
|
||||
|
||||
expandDirectory: ->
|
||||
selectedEntry = @selectedEntry()
|
||||
selectedEntry.view().expand() if (selectedEntry instanceof DirectoryView)
|
||||
|
||||
collapseDirectory: ->
|
||||
selectedEntry = @selectedEntry()
|
||||
if directory = selectedEntry.closest('.expanded.directory').view()
|
||||
directory.collapse()
|
||||
@selectEntry(directory)
|
||||
|
||||
openSelectedEntry: ->
|
||||
selectedEntry = @selectedEntry()
|
||||
if (selectedEntry instanceof DirectoryView)
|
||||
selectedEntry.view().toggleExpansion()
|
||||
else if (selectedEntry instanceof FileView)
|
||||
@rootView.open(selectedEntry.getPath(), changeFocus: false)
|
||||
|
||||
moveSelectedEntry: ->
|
||||
entry = @selectedEntry()
|
||||
return unless entry
|
||||
oldPath = @selectedEntry().getPath()
|
||||
|
||||
dialog = new Dialog
|
||||
prompt: "Enter the new path for the file:"
|
||||
path: @rootView.project.relativize(oldPath)
|
||||
select: true
|
||||
onConfirm: (newPath) =>
|
||||
newPath = @rootView.project.resolve(newPath)
|
||||
directoryPath = fs.directory(newPath)
|
||||
try
|
||||
fs.makeTree(directoryPath) unless fs.exists(directoryPath)
|
||||
fs.move(oldPath, newPath)
|
||||
catch e
|
||||
dialog.showError("Error: " + e.message + " Try a different path:")
|
||||
return false
|
||||
|
||||
@rootView.append(dialog)
|
||||
|
||||
removeSelectedEntry: ->
|
||||
entry = @selectedEntry()
|
||||
return unless entry
|
||||
|
||||
entryType = if entry instanceof DirectoryView then "directory" else "file"
|
||||
message = "Are you sure you would like to delete the selected #{entryType}?"
|
||||
detailedMessage = "You are deleting #{entry.getPath()}"
|
||||
buttons = [
|
||||
["Move to Trash", => Native.moveToTrash(entry.getPath())]
|
||||
["Cancel", => ] # Do Nothing
|
||||
["Delete", => fs.remove(entry.getPath())]
|
||||
]
|
||||
|
||||
Native.alert message, detailedMessage, buttons
|
||||
|
||||
add: ->
|
||||
selectedPath = @selectedEntry().getPath()
|
||||
directoryPath = if fs.isFile(selectedPath) then fs.directory(selectedPath) else selectedPath
|
||||
relativeDirectoryPath = @rootView.project.relativize(directoryPath)
|
||||
relativeDirectoryPath += '/' if relativeDirectoryPath.length > 0
|
||||
|
||||
dialog = new Dialog
|
||||
prompt: "Enter the path for the new file/directory. Directories end with '/':"
|
||||
path: relativeDirectoryPath
|
||||
select: false
|
||||
onConfirm: (relativePath) =>
|
||||
endsWithDirectorySeperator = /\/$/.test(relativePath)
|
||||
path = @rootView.project.resolve(relativePath)
|
||||
try
|
||||
if fs.exists(path)
|
||||
pathType = if fs.isFile(path) then "file" else "directory"
|
||||
dialog.showError("Error: A #{pathType} already exists at path '#{path}'. Try a different path:")
|
||||
false
|
||||
else if endsWithDirectorySeperator
|
||||
fs.makeTree(path)
|
||||
else
|
||||
fs.write(path, "")
|
||||
@rootView.open(path)
|
||||
catch e
|
||||
dialog.showError("Error: " + e.message + " Try a different path:")
|
||||
return false
|
||||
|
||||
@rootView.append(dialog)
|
||||
|
||||
selectedEntry: ->
|
||||
@find('.selected')?.view()
|
||||
|
||||
selectEntry: (entry) ->
|
||||
return false unless entry.get(0)
|
||||
@find('.selected').removeClass('selected')
|
||||
entry.addClass('selected')
|
||||
|
||||
scrollToEntry: (entry) ->
|
||||
displayElement = if (entry instanceof DirectoryView) then entry.header else entry
|
||||
|
||||
top = @scrollTop() + displayElement.position().top
|
||||
bottom = top + displayElement.outerHeight()
|
||||
|
||||
if bottom > @scrollBottom()
|
||||
@scrollBottom(bottom)
|
||||
if top < @scrollTop()
|
||||
@scrollTop(top)
|
||||
10
src/extensions/vim-mode/commands.coffee
Normal file
10
src/extensions/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 }
|
||||
|
||||
130
src/extensions/vim-mode/index.coffee
Normal file
130
src/extensions/vim-mode/index.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
|
||||
89
src/extensions/vim-mode/motions.coffee
Normal file
89
src/extensions/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.getBuffer().getLineCount() - 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.lineForBufferRow(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.getBuffer().getLineCount()
|
||||
column = @editor.lineForBufferRow(row).length
|
||||
else
|
||||
nextLineMatch = regex.exec(@editor.lineForBufferRow(++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.getLastBufferRow()]
|
||||
if @editor.lineForBufferRow(r).length == 0
|
||||
row = r
|
||||
break
|
||||
|
||||
if not row
|
||||
row = @editor.getLastBufferRow()
|
||||
column = @editor.getBuffer().getLastLine().length - 1
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
module.exports = { Motion, MoveLeft, MoveRight, MoveUp, MoveDown, MoveToNextWord, MoveToPreviousWord, MoveToNextParagraph }
|
||||
57
src/extensions/vim-mode/operators.coffee
Normal file
57
src/extensions/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.getBuffer().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 }
|
||||
|
||||
3
src/single-window-bootstrap.coffee
Normal file
3
src/single-window-bootstrap.coffee
Normal file
@@ -0,0 +1,3 @@
|
||||
# Bootstrap the app in a single window mode
|
||||
require 'atom-bootstrap.coffee'
|
||||
require 'window-bootstrap.coffee'
|
||||
35
src/stdlib/child-process.coffee
Normal file
35
src/stdlib/child-process.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
# node.js child-process
|
||||
# http://nodejs.org/docs/v0.6.3/api/child_processes.html
|
||||
|
||||
$ = require 'jquery'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class ChildProccess
|
||||
@exec: (command, options={}) ->
|
||||
deferred = $.Deferred()
|
||||
|
||||
if options.bufferLines
|
||||
options.stdout = @bufferLines(options.stdout) if options.stdout
|
||||
options.stderr = @bufferLines(options.stderr) if options.stderr
|
||||
|
||||
$native.exec command, options, (exitStatus, stdout, stderr) ->
|
||||
try
|
||||
if exitStatus != 0
|
||||
deferred.reject({command, exitStatus, stderr})
|
||||
else
|
||||
deferred.resolve(stdout, stderr)
|
||||
catch e
|
||||
console.error "In ChildProccess termination callback: ", e.message
|
||||
console.error e.stack
|
||||
|
||||
deferred
|
||||
|
||||
@bufferLines: (callback) ->
|
||||
buffered = ""
|
||||
(data) ->
|
||||
buffered += data
|
||||
lastNewlineIndex = buffered.lastIndexOf('\n')
|
||||
if lastNewlineIndex >= 0
|
||||
callback(buffered.substring(0, lastNewlineIndex + 1))
|
||||
buffered = buffered.substring(lastNewlineIndex + 1)
|
||||
20
src/stdlib/event.coffee
Normal file
20
src/stdlib/event.coffee
Normal file
@@ -0,0 +1,20 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
class Event
|
||||
events: {}
|
||||
|
||||
on: (name, callback) ->
|
||||
@events[name] ?= []
|
||||
@events[name].push callback
|
||||
|
||||
off: (name, callback) ->
|
||||
delete @events[name][_.indexOf callback] if @events[name]
|
||||
|
||||
trigger: (name, data...) ->
|
||||
if name.match /^app:/
|
||||
OSX.NSApp.triggerGlobalAtomEvent_data name, data
|
||||
return
|
||||
|
||||
_.each @events[name], (callback) => callback data...
|
||||
null
|
||||
131
src/stdlib/fs.coffee
Normal file
131
src/stdlib/fs.coffee
Normal file
@@ -0,0 +1,131 @@
|
||||
# commonjs fs module
|
||||
# http://ringojs.org/api/v0.8/fs/
|
||||
|
||||
_ = require 'underscore'
|
||||
$ = require 'jquery'
|
||||
|
||||
module.exports =
|
||||
# Make the given path absolute by resolving it against the
|
||||
# current working directory.
|
||||
absolute: (path) ->
|
||||
$native.absolute(path)
|
||||
|
||||
# Return the basename of the given path. That is the path with
|
||||
# any leading directory components removed. If specified, also
|
||||
# remove a trailing extension.
|
||||
base: (path, ext) ->
|
||||
base = path.replace(/\/$/, '').split("/").pop()
|
||||
if ext then base.replace(RegExp(ext + "$"), "") else base
|
||||
|
||||
# Returns the path of a file's containing directory, albeit the
|
||||
# parent directory if the file is a directory. A terminal directory
|
||||
# separator is ignored.
|
||||
directory: (path) ->
|
||||
parentPath = path.replace(new RegExp("/#{@base(path)}\/?$"), '')
|
||||
return "" if path == parentPath
|
||||
parentPath
|
||||
|
||||
# Returns true if the file specified by path exists
|
||||
exists: (path) ->
|
||||
return false unless path?
|
||||
$native.exists(path)
|
||||
|
||||
# Returns the extension of a file. The extension of a file is the
|
||||
# last dot (excluding any number of initial dots) followed by one or
|
||||
# more non-dot characters. Returns an empty string if no valid
|
||||
# extension exists.
|
||||
extension: (path) ->
|
||||
return '' unless typeof path is 'string'
|
||||
match = @base(path).match(/\.[^\.]+$/)
|
||||
if match
|
||||
match[0]
|
||||
else
|
||||
""
|
||||
|
||||
join: (paths...) ->
|
||||
return paths[0] if paths.length == 1
|
||||
[first, rest...] = paths
|
||||
first.replace(/\/?$/, "/") + @join(rest...)
|
||||
|
||||
# Returns true if the file specified by path exists and is a
|
||||
# directory.
|
||||
isDirectory: (path) ->
|
||||
$native.isDirectory path
|
||||
|
||||
# Returns true if the file specified by path exists and is a
|
||||
# regular file.
|
||||
isFile: (path) ->
|
||||
return false unless path?
|
||||
$native.isFile(path)
|
||||
|
||||
# Returns an array with all the names of files contained
|
||||
# in the directory path.
|
||||
list: (path) ->
|
||||
$native.list(path, false)
|
||||
|
||||
listTree: (path) ->
|
||||
$native.list(path, true)
|
||||
|
||||
move: (source, target) ->
|
||||
$native.move(source, target)
|
||||
|
||||
# Remove a file at the given path. Throws an error if path is not a
|
||||
# file or a symbolic link to a file.
|
||||
remove: (path) ->
|
||||
$native.remove path
|
||||
|
||||
# Open, read, and close a file, returning the file's contents.
|
||||
read: (path) ->
|
||||
$native.read(path)
|
||||
|
||||
# Returns an array of path components. If the path is absolute, the first
|
||||
# component will be an indicator of the root of the file system; for file
|
||||
# systems with drives (such as Windows), this is the drive identifier with a
|
||||
# colon, like "c:"; on Unix, this is an empty string "". The intent is that
|
||||
# calling "join.apply" with the result of "split" as arguments will
|
||||
# reconstruct the path.
|
||||
split: (path) ->
|
||||
path.split("/")
|
||||
|
||||
# Open, write, flush, and close a file, writing the given content.
|
||||
write: (path, content) ->
|
||||
$native.write(path, content)
|
||||
|
||||
makeDirectory: (path) ->
|
||||
$native.makeDirectory(path)
|
||||
|
||||
# Creates the directory specified by "path" including any missing parent
|
||||
# directories.
|
||||
makeTree: (path) ->
|
||||
return unless path
|
||||
if not @exists(path)
|
||||
@makeTree(@directory(path))
|
||||
@makeDirectory(path)
|
||||
|
||||
traverseTree: (rootPath, fn) ->
|
||||
recurse = null
|
||||
prune = -> recurse = false
|
||||
|
||||
for path in @list(rootPath)
|
||||
recurse = true
|
||||
fn(path, prune)
|
||||
@traverseTree(path, fn) if @isDirectory(path) and recurse
|
||||
|
||||
lastModified: (path) ->
|
||||
$native.lastModified(path)
|
||||
|
||||
md5ForPath: (path) ->
|
||||
$native.md5ForPath(path)
|
||||
|
||||
async:
|
||||
list: (path) ->
|
||||
deferred = $.Deferred()
|
||||
$native.asyncList path, false, (subpaths) ->
|
||||
deferred.resolve subpaths
|
||||
deferred
|
||||
|
||||
listTree: (path) ->
|
||||
deferred = $.Deferred()
|
||||
$native.asyncList path, true, (subpaths) ->
|
||||
deferred.resolve subpaths
|
||||
deferred
|
||||
16
src/stdlib/fuzzy-filter.coffee
Normal file
16
src/stdlib/fuzzy-filter.coffee
Normal file
@@ -0,0 +1,16 @@
|
||||
stringScore = require 'stringscore'
|
||||
|
||||
module.exports = (candidates, query, options) ->
|
||||
if query
|
||||
scoredCandidates = candidates.map (candidate) ->
|
||||
string = if options.key? then candidate[options.key] else candidate
|
||||
{ candidate, score: stringScore(string, query) }
|
||||
|
||||
scoredCandidates.sort (a, b) ->
|
||||
if a.score > b.score then -1
|
||||
else if a.score < b.score then 1
|
||||
else 0
|
||||
candidates = (scoredCandidate.candidate for scoredCandidate in scoredCandidates when scoredCandidate.score > 0)
|
||||
|
||||
candidates = candidates[0...options.maxResults] if options.maxResults?
|
||||
candidates
|
||||
32
src/stdlib/jquery-extensions.coffee
Normal file
32
src/stdlib/jquery-extensions.coffee
Normal file
@@ -0,0 +1,32 @@
|
||||
$ = require 'jquery'
|
||||
|
||||
$.fn.scrollBottom = (newValue) ->
|
||||
if newValue?
|
||||
@scrollTop(newValue - @height())
|
||||
else
|
||||
@scrollTop() + @height()
|
||||
|
||||
$.fn.scrollRight = (newValue) ->
|
||||
if newValue?
|
||||
@scrollLeft(newValue - @width())
|
||||
else
|
||||
@scrollLeft() + @width()
|
||||
|
||||
$.fn.containsElement = (element) ->
|
||||
(element[0].compareDocumentPosition(this[0]) & 8) == 8
|
||||
|
||||
$.fn.preempt = (eventName, handler) ->
|
||||
@on eventName, (e, args...) ->
|
||||
if handler(e, args...) == false then e.stopImmediatePropagation()
|
||||
|
||||
eventNameWithoutNamespace = eventName.split('.')[0]
|
||||
handlers = @data('events')[eventNameWithoutNamespace]
|
||||
handlers.unshift(handlers.pop())
|
||||
|
||||
$.fn.hasParent = ->
|
||||
@parent()[0]?
|
||||
|
||||
$.fn.flashError = ->
|
||||
@addClass 'error'
|
||||
removeErrorClass = => @removeClass 'error'
|
||||
window.setTimeout(removeErrorClass, 200)
|
||||
9
src/stdlib/native.coffee
Normal file
9
src/stdlib/native.coffee
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports =
|
||||
class Native
|
||||
@alert: (args...) -> $native.alert(args...)
|
||||
|
||||
@saveDialog: (args...) -> $native.saveDialog(args...)
|
||||
|
||||
@reload: -> $native.reload()
|
||||
|
||||
@moveToTrash: (args...) -> $native.moveToTrash(args...)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user