Merge branch 'editor'

This commit is contained in:
Corey Johnson & Nathan Sobo
2012-02-02 17:06:18 -07:00
8 changed files with 351 additions and 56 deletions

View File

@@ -102,6 +102,24 @@ describe 'Buffer', ->
expect(buffer.getLine(3)).toBe " var pivot = sort(Array.apply(this, arguments));"
expect(buffer.getLine(4)).toBe "};"
describe ".setText(text)", ->
it "changes the entire contents of the buffer and emits a change event", ->
lastRow = buffer.lastRow()
expectedPreRange = new Range([0,0], [lastRow, buffer.getLine(lastRow).length])
changeHandler = jasmine.createSpy('changeHandler')
buffer.on 'change', changeHandler
newText = "I know you are.\nBut what am I?"
buffer.setText(newText)
expect(buffer.getText()).toBe newText
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
expect(event.string).toBe newText
expect(event.preRange).toEqual expectedPreRange
expect(event.postRange).toEqual(new Range([0, 0], [1, 14]))
describe ".save()", ->
describe "when the buffer has a path", ->
filePath = null

View File

@@ -25,6 +25,26 @@ describe "Editor", ->
expect(buffer.getLine(10)).toBe ''
expect(editor.lines.find('pre:eq(10)').html()).toBe ' '
it "syntax highlights code based on the file type", ->
line1 = editor.lines.find('.line:first')
expect(line1.find('span:eq(0)')).toMatchSelector '.keyword.definition'
expect(line1.find('span:eq(0)').text()).toBe 'var'
expect(line1.find('span:eq(1)')).toMatchSelector '.text'
expect(line1.find('span:eq(1)').text()).toBe ' '
expect(line1.find('span:eq(2)')).toMatchSelector '.identifier'
expect(line1.find('span:eq(2)').text()).toBe 'quicksort'
expect(line1.find('span:eq(4)')).toMatchSelector '.operator'
expect(line1.find('span:eq(4)').text()).toBe '='
line12 = editor.lines.find('.line:eq(11)')
expect(line12.find('span:eq(1)')).toMatchSelector '.keyword'
describe "when lines are updated in the buffer", ->
it "syntax highlights the updated lines", ->
expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition'
buffer.insert([0, 4], "g")
expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition'
describe "cursor movement", ->
describe ".setCursorPosition({row, column})", ->
beforeEach ->

View File

@@ -0,0 +1,69 @@
Highlighter = require 'highlighter'
Buffer = require 'buffer'
Range = require 'range'
describe "Highlighter", ->
[highlighter, buffer] = []
beforeEach ->
buffer = new Buffer(require.resolve('fixtures/sample.js'))
highlighter = new Highlighter(buffer)
describe "constructor", ->
it "tokenizes all the lines in the buffer", ->
expect(highlighter.tokensForRow(0)[0]).toEqual(type: 'keyword.definition', value: 'var')
expect(highlighter.tokensForRow(11)[1]).toEqual(type: 'keyword', value: 'return')
describe "when the buffer changes", ->
describe "when lines are updated, but none are added or removed", ->
it "updates tokens for each of the changed lines", ->
buffer.change(new Range([0, 0], [2, 0]), "foo()\nbar()\n")
expect(highlighter.tokensForRow(0)[0]).toEqual(type: 'identifier', value: 'foo')
expect(highlighter.tokensForRow(1)[0]).toEqual(type: 'identifier', value: 'bar')
# line 2 is unchanged
expect(highlighter.tokensForRow(2)[1]).toEqual(type: 'keyword', value: 'if')
it "updates tokens for lines beyond the changed lines if needed", ->
buffer.insert([5, 30], '/* */')
buffer.insert([2, 0], '/*')
expect(highlighter.tokensForRow(3)[0].type).toBe 'comment'
expect(highlighter.tokensForRow(4)[0].type).toBe 'comment'
expect(highlighter.tokensForRow(5)[0].type).toBe 'comment'
describe "when lines are both updated and removed", ->
it "updates tokens to reflect the removed lines", ->
buffer.change(new Range([1, 0], [3, 0]), "foo()")
# previous line 0 remains
expect(highlighter.tokensForRow(0)[0]).toEqual(type: 'keyword.definition', value: 'var')
# previous line 3 should be combined with input to form line 1
expect(highlighter.tokensForRow(1)[0]).toEqual(type: 'identifier', value: 'foo')
expect(highlighter.tokensForRow(1)[6]).toEqual(type: 'identifier', value: 'pivot')
# lines below deleted regions should be shifted upward
expect(highlighter.tokensForRow(2)[1]).toEqual(type: 'keyword', value: 'while')
expect(highlighter.tokensForRow(3)[1]).toEqual(type: 'identifier', value: 'current')
expect(highlighter.tokensForRow(4)[3]).toEqual(type: 'keyword.operator', value: '<')
describe "when lines are both updated and inserted", ->
it "updates tokens to reflect the inserted lines", ->
buffer.change(new Range([1, 0], [2, 0]), "foo()\nbar()\nbaz()\nquux()")
# previous line 0 remains
expect(highlighter.tokensForRow(0)[0]).toEqual(type: 'keyword.definition', value: 'var')
# 3 new lines inserted
expect(highlighter.tokensForRow(1)[0]).toEqual(type: 'identifier', value: 'foo')
expect(highlighter.tokensForRow(2)[0]).toEqual(type: 'identifier', value: 'bar')
expect(highlighter.tokensForRow(3)[0]).toEqual(type: 'identifier', value: 'baz')
# previous line 2 is joined with quux() on line 4
expect(highlighter.tokensForRow(4)[0]).toEqual(type: 'identifier', value: 'quux')
expect(highlighter.tokensForRow(4)[4]).toEqual(type: 'keyword', value: 'if')
# previous line 3 is pushed down to become line 5
expect(highlighter.tokensForRow(5)[3]).toEqual(type: 'identifier', value: 'pivot')

View File

@@ -1,4 +1,6 @@
_ = require 'underscore'
fs = require 'fs'
Range = require 'range'
module.exports =
class Buffer
@@ -6,6 +8,7 @@ class Buffer
constructor: (@path) ->
@url = @path # we want this to be path on master, but let's not break it on a branch
@lines = ['']
if @path and fs.exists(@path)
@setText(fs.read(@path))
else
@@ -15,7 +18,10 @@ class Buffer
@lines.join('\n')
setText: (text) ->
@lines = text.split('\n')
@change(@getRange(), text)
getRange: ->
new Range([0, 0], [@lastRow(), @lastLine().length])
getTextInRange: (range) ->
if range.start.row == range.end.row
@@ -35,46 +41,38 @@ class Buffer
getLine: (row) ->
@lines[row]
change: (preRange, string) ->
@remove(preRange)
postRange = @insert(preRange.start, string)
@trigger 'change', { preRange, postRange, string }
remove: (range) ->
prefix = @lines[range.start.row][0...range.start.column]
suffix = @lines[range.end.row][range.end.column..]
@lines[range.start.row..range.end.row] = prefix + suffix
insert: ({row, column}, string) ->
postRange =
start: { row, column }
end: { row, column }
prefix = @lines[row][0...column]
suffix = @lines[row][column..]
lines = string.split('\n')
if lines.length == 1
@lines[row] = prefix + string + suffix
postRange.end.column += string.length
else
for line, i in lines
curRow = row + i
if i == 0 # replace first line
@lines[curRow] = prefix + line
else if i < lines.length - 1 # insert middle lines
@lines[curRow...curRow] = line
else # insert last line
@lines[curRow...curRow] = line + suffix
postRange.end.row = curRow
postRange.end.column = line.length
postRange
numLines: ->
@getLines().length
lastRow: ->
@getLines().length - 1
lastLine: ->
@getLine(@lastRow())
insert: (point, text) ->
@change(new Range(point, point), text)
change: (preRange, newText) ->
postRange = new Range(_.clone(preRange.start), _.clone(preRange.start))
prefix = @lines[preRange.start.row][0...preRange.start.column]
suffix = @lines[preRange.end.row][preRange.end.column..]
newTextLines = newText.split('\n')
if newTextLines.length == 1
postRange.end.column += newText.length
newTextLines = [prefix + newText + suffix]
else
lastLineIndex = newTextLines.length - 1
newTextLines[0] = prefix + newTextLines[0]
postRange.end.row += lastLineIndex
postRange.end.column = newTextLines[lastLineIndex].length
newTextLines[lastLineIndex] += suffix
@lines[preRange.start.row..preRange.end.row] = newTextLines
@trigger 'change', { preRange, postRange, string: newText }
save: ->
if not @path then throw new Error("Tried to save buffer with no url")
fs.write @path, @getText()
@@ -87,3 +85,14 @@ class Buffer
trigger: (eventName, event) ->
@eventHandlers?[eventName]?.forEach (handler) -> handler(event)
modeName: ->
extension = if @path then @path.split('/').pop().split('.').pop() else null
switch extension
when 'js' then 'javascript'
when 'coffee' then 'coffee'
when 'rb', 'ru' then 'ruby'
when 'c', 'h', 'cpp' then 'c_cpp'
when 'html', 'htm' then 'html'
when 'css' then 'css'
else 'text'

View File

@@ -3,7 +3,9 @@ Buffer = require 'buffer'
Point = require 'point'
Cursor = require 'cursor'
Selection = require 'selection'
Highlighter = require 'highlighter'
Range = require 'range'
$ = require 'jquery'
$$ = require 'template/builder'
_ = require 'underscore'
@@ -26,6 +28,7 @@ class Editor extends Template
initialize: () ->
requireStylesheet 'editor.css'
requireStylesheet 'theme/twilight.css'
@bindKeys()
@buildCursorAndSelection()
@handleEvents()
@@ -97,16 +100,23 @@ class Editor extends Template
@hiddenInput.width(@charWidth)
@focus()
buildLineElement: (lineText) ->
if lineText is ''
$$.pre class: "line", -> @raw('&nbsp;')
else
$$.pre class: "line", lineText
buildLineElement: (row) ->
tokens = @highlighter.tokensForRow(row)
$$.pre class: 'line', ->
if tokens.length
for token in tokens
classes = token.type.split('.').map((c) -> "ace_#{c}").join(' ')
@span { class: token.type.replace('.', ' ') }, token.value
else
@raw '&nbsp;'
setBuffer: (@buffer) ->
@highlighter = new Highlighter(@buffer)
@lines.empty()
for line in @buffer.getLines()
@lines.append @buildLineElement(line)
for row in [0..@buffer.lastRow()]
line = @buildLineElement(row)
@lines.append line
@setCursorPosition(row: 0, column: 0)
@@ -128,18 +138,13 @@ class Editor extends Template
else
@updateLineElement(row)
@selection.bufferChanged(e)
@cursor.bufferChanged(e)
updateLineElement: (row) ->
line = @buffer.getLine(row)
element = @getLineElement(row)
if line == ''
element.html('&nbsp;')
else
element.text(line)
@getLineElement(row).replaceWith(@buildLineElement(row))
insertLineElement: (row) ->
@getLineElement(row).before(@buildLineElement(@buffer.getLine(row)))
@getLineElement(row).before(@buildLineElement(row))
removeLineElement: (row) ->
@getLineElement(row).remove()

View File

@@ -0,0 +1,43 @@
_ = require 'underscore'
module.exports =
class Highlighter
buffer: null
tokenizer: null
lines: []
constructor: (@buffer) ->
@buildTokenizer()
@lines = @tokenizeRows('start', 0, @buffer.lastRow())
@buffer.on 'change', (e) => @handleBufferChange(e)
buildTokenizer: ->
Mode = require("ace/mode/#{@buffer.modeName()}").Mode
@tokenizer = (new Mode).getTokenizer()
handleBufferChange: (e) ->
{ preRange, postRange } = e
previousState = @lines[preRange.end.row].state
newLines = @tokenizeRows('start', postRange.start.row, postRange.end.row)
@lines[preRange.start.row..preRange.end.row] = newLines
for row in [postRange.end.row...@buffer.lastRow()]
break if @lines[row].state == previousState
nextRow = row + 1
previousState = @lines[nextRow].state
@lines[nextRow] = @tokenizeRow(@lines[row].state, nextRow)
tokenizeRows: (startState, startRow, endRow) ->
state = startState
for row in [startRow..endRow]
line = @tokenizeRow(state, row)
state = line.state
line
tokenizeRow: (state, row) ->
@tokenizer.getLineTokens(@buffer.getLine(row), state)
tokensForRow: (row) ->
@lines[row].tokens

View File

@@ -26,9 +26,6 @@ class Selection extends Template
@anchor = null
@updateAppearance()
bufferChanged: (e) ->
@cursor.setPosition(e.postRange.end)
updateAppearance: ->
@clearRegions()

134
static/theme/twilight.css Normal file
View File

@@ -0,0 +1,134 @@
.editor {
border: 2px solid rgb(159, 159, 159);
}
.editor.focus {
border: 2px solid #327fbd;
}
.gutter {
background: #e8e8e8;
color: #333;
}
.print_margin {
width: 1px;
background: #e8e8e8;
}
.scroller {
background-color: #141414;
}
.text-layer {
cursor: text;
color: #F8F8F8;
}
.cursor {
border-left: 2px solid #A7A7A7;
}
.cursor.overwrite {
border-left: 0px;
border-bottom: 1px solid #A7A7A7;
}
.marker-layer .selection {
background: rgba(221, 240, 255, 0.20);
}
.marker-layer .step {
background: rgb(198, 219, 174);
}
.marker-layer .bracket {
margin: -1px 0 0 -1px;
border: 1px solid rgba(255, 255, 255, 0.25);
}
.marker-layer .active_line {
background: rgba(255, 255, 255, 0.031);
}
.marker-layer .selected_word {
border: 1px solid rgba(221, 240, 255, 0.20);
}
.invisible {
color: rgba(255, 255, 255, 0.25);
}
.keyword {
color:#CDA869;
}
.constant {
color:#CF6A4C;
}
.invalid.illegal {
color:#F8F8F8;
background-color:rgba(86, 45, 86, 0.75);
}
.invalid.deprecated {
text-decoration:underline;
font-style:italic;
color:#D2A8A1;
}
.support {
color:#9B859D;
}
.fold {
background-color: #AC885B;
border-color: #F8F8F8;
}
.support.function {
color:#DAD085;
}
.string {
color:#8F9D6A;
}
.string.regexp {
color:#E9C062;
}
.comment {
font-style:italic;
color:#5F5A60;
}
.variable {
color:#7587A6;
}
.xml_pe {
color:#494949;
}
.meta.tag {
color:#AC885B;
}
.entity.name.function {
color:#AC885B;
}
.markup.underline {
text-decoration:underline;
}
.markup.heading {
color:#CF6A4C;
}
.markup.list {
color:#F9EE98;
}";