mirror of
https://github.com/atom/atom.git
synced 2026-01-24 06:18:03 -05:00
Merge branch 'editor'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
69
spec/atom/highlighter-spec.coffee
Normal file
69
spec/atom/highlighter-spec.coffee
Normal 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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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(' ')
|
||||
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 ' '
|
||||
|
||||
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(' ')
|
||||
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()
|
||||
|
||||
43
src/atom/highlighter.coffee
Normal file
43
src/atom/highlighter.coffee
Normal 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
|
||||
|
||||
@@ -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
134
static/theme/twilight.css
Normal 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;
|
||||
}";
|
||||
|
||||
Reference in New Issue
Block a user