editor_core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +4 -3
- data/lib/editor_core/buffer.rb +133 -0
- data/lib/editor_core/core.rb +189 -0
- data/lib/editor_core/cursor.rb +72 -0
- data/lib/editor_core/history.rb +42 -0
- data/lib/editor_core/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec52d9db82e48638cbc82762edbfc019f825beda5eafa66475f18254c69e1f4e
|
4
|
+
data.tar.gz: 354a5ae6fb35b9bb195df0ace5c579a1643b61dbd0e8449ce7428c62b9fefd98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d65ef8b4ca8ca3138a67e0684bd3f315569423d715fc6fff022edb2d1c3b717db1efedfb4892711992107056c9afe29e30114beef2fb7c4d3c5350ea3ea8ba1
|
7
|
+
data.tar.gz: 483e3d2feb7dda1c78b5634612b92eb3f946c86921ae7898a81a3c73b665fd2d26c1edac0a724faed232b4ee190fcc1c3a1f37f72e979862132c95c400262775
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# EditorCore
|
2
2
|
|
3
|
-
|
3
|
+
Very basic core concepts for an editor. Used in the next iteration of
|
4
|
+
[Re](https://github.com/vidarh/re)
|
4
5
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -32,7 +32,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
32
|
|
33
33
|
## Contributing
|
34
34
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at
|
35
|
+
Bug reports and pull requests are welcome on GitHub at
|
36
|
+
https://github.com/vidarh/editor_core.
|
36
37
|
|
37
38
|
|
38
39
|
## License
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'drb/observer'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
require_relative 'history'
|
5
|
+
|
6
|
+
module EditorCore
|
7
|
+
class Buffer
|
8
|
+
|
9
|
+
include DRb::DRbObservable
|
10
|
+
|
11
|
+
attr_reader :name, :created_at, :history
|
12
|
+
attr_accessor :buffer_id
|
13
|
+
attr_writer :created_at
|
14
|
+
|
15
|
+
def initialize(id,name, lines, created_at = 0)
|
16
|
+
@buffer_id = id # An id for this buffer unique for this session
|
17
|
+
@name = name
|
18
|
+
@history = History.new
|
19
|
+
@lines = lines
|
20
|
+
@created_at = case created_at
|
21
|
+
when Numeric
|
22
|
+
Time.at(created_at)
|
23
|
+
when Time
|
24
|
+
created_at
|
25
|
+
when String
|
26
|
+
DateTime.parse(created_at)
|
27
|
+
else
|
28
|
+
raise "Unknown time format: #{created_at}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def as_json(options = { })
|
33
|
+
{
|
34
|
+
"json_class" => self.class.name,
|
35
|
+
"data" => {
|
36
|
+
"id" => @buffer_id,
|
37
|
+
"name" => @name,
|
38
|
+
"lines" => @lines,
|
39
|
+
"created_at" => Time.at(created_at.to_i)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json(*a)
|
45
|
+
as_json.to_json(*a)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.json_create(o)
|
49
|
+
d = o["data"]
|
50
|
+
new(d["id"], d["name"], d["lines"], (d["created_at"] || d["modified_at"] || 0))
|
51
|
+
end
|
52
|
+
|
53
|
+
def lines_count
|
54
|
+
@lines.size
|
55
|
+
end
|
56
|
+
|
57
|
+
def line_length(row)
|
58
|
+
@lines[row]&.size || 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def replace_contents(cursor,new_contents)
|
62
|
+
modify(cursor, 0..-1) {|l| new_contents }
|
63
|
+
end
|
64
|
+
|
65
|
+
def insert(cursor, char)
|
66
|
+
modify(cursor, cursor.row) {|l| l.insert(cursor.col,char) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(cursor, from, to =nil)
|
70
|
+
to ||= from
|
71
|
+
modify(cursor, cursor.row) {|l| l[from..to] = ''; l }
|
72
|
+
end
|
73
|
+
|
74
|
+
def break_line(cursor)
|
75
|
+
modify(cursor, cursor.row..cursor.row) do |l|
|
76
|
+
[l[0][0...cursor.col], l[0][cursor.col..-1]]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def join_lines(cursor,offset=0)
|
81
|
+
row=cursor.row+offset
|
82
|
+
modify(cursor, row..row+1) {|l| l.join }
|
83
|
+
end
|
84
|
+
|
85
|
+
def indent(cursor, row, pos)
|
86
|
+
modify(cursor,row) do |line|
|
87
|
+
(" "*pos)+line.lstrip
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def lines(r)
|
92
|
+
@lines[r]
|
93
|
+
end
|
94
|
+
|
95
|
+
def can_undo?
|
96
|
+
@history.can_undo?
|
97
|
+
end
|
98
|
+
|
99
|
+
def can_redo?
|
100
|
+
@history.can_redo?
|
101
|
+
end
|
102
|
+
|
103
|
+
def undo(old_cursor)
|
104
|
+
cursor, rowrange, rows = @history.undo_snapshot
|
105
|
+
store_snapshot(old_cursor,rowrange, false)
|
106
|
+
cursor, rowrange, rows = @history.undo
|
107
|
+
@lines[rowrange] = rows
|
108
|
+
cursor
|
109
|
+
end
|
110
|
+
|
111
|
+
def redo
|
112
|
+
cursor, rowrange, rows = @history.redo
|
113
|
+
@lines[rowrange] = rows
|
114
|
+
cursor
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def store_snapshot(cursor, rowrange, advance = true)
|
120
|
+
@history.save([cursor, rowrange, @lines[rowrange]], advance)
|
121
|
+
end
|
122
|
+
|
123
|
+
def modify(cursor, rowrange)
|
124
|
+
lines = @lines[rowrange].dup
|
125
|
+
lines ||= ""
|
126
|
+
new_lines = yield(lines)
|
127
|
+
store_snapshot(cursor, rowrange, true)
|
128
|
+
@lines[rowrange] = new_lines
|
129
|
+
changed
|
130
|
+
notify_observers(self)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
#
|
2
|
+
# # Core
|
3
|
+
#
|
4
|
+
# Generic editor functionality that only depends on `Buffer`, `Cursor`
|
5
|
+
# and bare minimum of `View`
|
6
|
+
#
|
7
|
+
# Functionality that goes here should *only* be "objective" behaviours.
|
8
|
+
# E.g. what it means to move a cursor to the right is mostly objective.
|
9
|
+
# What should happen on enter probably isn't (may or may not include
|
10
|
+
# indenting the next line, may or may not involve stripping trailing
|
11
|
+
# whitespace, etc.). Methods for the objective, constituent parts of
|
12
|
+
# subjective/opinionated operations *can* be added here (e.g. an "enter"
|
13
|
+
# method which defers the subjective parts to a block or some hook
|
14
|
+
# mechanism could.
|
15
|
+
#
|
16
|
+
# Knows nothing of configuration or "fancier" options, which should
|
17
|
+
# generally be handled in a subclass
|
18
|
+
#
|
19
|
+
# `@view` must support `#top`/`#top=` and `#height` to control the
|
20
|
+
# viewable portion.
|
21
|
+
#
|
22
|
+
module EditorCore
|
23
|
+
class Core
|
24
|
+
attr_accessor :cursor, :buffer, :view
|
25
|
+
|
26
|
+
# ## Basic cursor movement ##
|
27
|
+
|
28
|
+
def move(row, col); @cursor = cursor.move(buffer, row.to_i, col.to_i) end
|
29
|
+
def right(off=1); @cursor = cursor.right(buffer,off.to_i) end
|
30
|
+
def left(off=1); @cursor = cursor.left(buffer,off.to_i) end
|
31
|
+
def up(off=1); @cursor = cursor.up(buffer,off.to_i) end
|
32
|
+
def down(off=1); @cursor = cursor.down(buffer,off.to_i) end
|
33
|
+
|
34
|
+
def line_home; @cursor = cursor.line_home end
|
35
|
+
def line_end; @cursor = cursor.line_end(buffer) end
|
36
|
+
|
37
|
+
# ## Querying ##
|
38
|
+
|
39
|
+
def current_line
|
40
|
+
@buffer.lines(cursor.row)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_after
|
44
|
+
@buffer.lines(cursor.row)[@cursor.col..-1]
|
45
|
+
end
|
46
|
+
|
47
|
+
### View-relative cursor movement ##
|
48
|
+
|
49
|
+
def buffer_home
|
50
|
+
view_home
|
51
|
+
@cursor = cursor.move(buffer, 0, cursor.col)
|
52
|
+
end
|
53
|
+
|
54
|
+
def buffer_end
|
55
|
+
view_end
|
56
|
+
@cursor = cursor.move(buffer, buffer.lines_count, cursor.col)
|
57
|
+
end
|
58
|
+
|
59
|
+
def page_down
|
60
|
+
lines = view_down(view.height-1)
|
61
|
+
@cursor = cursor.down(buffer, lines)
|
62
|
+
end
|
63
|
+
|
64
|
+
def page_up
|
65
|
+
@cursor = cursor.up(buffer, view_up(view.height-1))
|
66
|
+
end
|
67
|
+
|
68
|
+
# View movement
|
69
|
+
def view_up(offset = 1)
|
70
|
+
oldtop = view.top
|
71
|
+
view.top -= offset
|
72
|
+
view.top = 0 if view.top < 0
|
73
|
+
oldtop - view.top
|
74
|
+
end
|
75
|
+
|
76
|
+
def view_down(offset = 1)
|
77
|
+
oldtop = view.top
|
78
|
+
view.top += offset
|
79
|
+
if view.top > buffer.lines_count
|
80
|
+
view.top = buffer.lines_count
|
81
|
+
end
|
82
|
+
view.top - oldtop
|
83
|
+
end
|
84
|
+
|
85
|
+
def view_home
|
86
|
+
view.top = 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def view_end
|
90
|
+
view.top = buffer.lines_count - view.height
|
91
|
+
end
|
92
|
+
|
93
|
+
# ## Complex navigation ##
|
94
|
+
def next_word
|
95
|
+
line = current_line
|
96
|
+
c = cursor.col
|
97
|
+
m = line.length
|
98
|
+
while c<m && line[c]&.match(/[^a-zA-Z]/)
|
99
|
+
c += 1
|
100
|
+
end
|
101
|
+
if c >= m
|
102
|
+
# FIXME: Pathological cases can cause stack issue here.
|
103
|
+
@cursor = Cursor.new(cursor.row,m)
|
104
|
+
right
|
105
|
+
return next_word
|
106
|
+
end
|
107
|
+
|
108
|
+
if run = line[c..-1]&.match(/([a-zA-Z]+)/)
|
109
|
+
c += run[0].length
|
110
|
+
#pry([line,c,run])
|
111
|
+
end
|
112
|
+
off = c - cursor.col
|
113
|
+
right(off)
|
114
|
+
off
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def prev_word
|
119
|
+
return if cursor.col == 0
|
120
|
+
line = current_line
|
121
|
+
c = cursor.col
|
122
|
+
if c > 0
|
123
|
+
c -= 1
|
124
|
+
end
|
125
|
+
while c > 0 && line[c] && line[c].match(/[ \t]/)
|
126
|
+
c -= 1
|
127
|
+
end
|
128
|
+
while c > 0 && !(line[c-1].match(/[ \t\-]/))
|
129
|
+
c -= 1
|
130
|
+
end
|
131
|
+
off = cursor.col - c
|
132
|
+
@cursor = Cursor.new(cursor.row, c)
|
133
|
+
off
|
134
|
+
end
|
135
|
+
|
136
|
+
# ## Mutation ##
|
137
|
+
|
138
|
+
def delete_before
|
139
|
+
buffer.delete(cursor, 0, cursor.col)
|
140
|
+
line_home
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete_after
|
144
|
+
buffer.delete(cursor, cursor.col,-1)
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete
|
148
|
+
return if cursor.end_of_file?(buffer)
|
149
|
+
|
150
|
+
if cursor.end_of_line?(buffer)
|
151
|
+
buffer.join_lines(cursor)
|
152
|
+
else
|
153
|
+
buffer.delete(cursor, cursor.col)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def join_line
|
158
|
+
buffer.join_lines(cursor)
|
159
|
+
end
|
160
|
+
|
161
|
+
def backspace
|
162
|
+
return if cursor.beginning_of_file?
|
163
|
+
|
164
|
+
if cursor.col == 0
|
165
|
+
cursor_left = buffer.lines(cursor.row).size + 1
|
166
|
+
buffer.join_lines(cursor,-1)
|
167
|
+
cursor_left.times { left }
|
168
|
+
else
|
169
|
+
buffer.delete(cursor, cursor.col - 1)
|
170
|
+
left
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
def rstrip_line
|
176
|
+
line = current_line
|
177
|
+
stripped = current_line.rstrip
|
178
|
+
return if line.length == stripped.length
|
179
|
+
col = cursor.col
|
180
|
+
oldc = cursor
|
181
|
+
move(cursor.row, stripped.length)
|
182
|
+
delete_after
|
183
|
+
if col < stripped.length
|
184
|
+
@cursor = oldc
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
module EditorCore
|
3
|
+
class Cursor
|
4
|
+
attr_reader :row, :col
|
5
|
+
|
6
|
+
def initialize(row = 0, col = 0)
|
7
|
+
@row = row
|
8
|
+
@col = col
|
9
|
+
freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def move(buffer,row,col); self.class.new(row,col).clamp(buffer); end
|
13
|
+
def up(buffer, offset = 1); move(buffer, row-offset,col); end
|
14
|
+
def down(buffer, offset = 1); move(buffer, row+offset,col); end
|
15
|
+
|
16
|
+
def left(buffer, offset = 1)
|
17
|
+
return Cursor.new(row, col - offset) if offset <= col
|
18
|
+
return self if beginning_of_file?
|
19
|
+
return move(buffer,row-1,buffer.line_length(row-1))
|
20
|
+
end
|
21
|
+
|
22
|
+
def right(buffer, offset = 1)
|
23
|
+
#buffer.right(self,offset)
|
24
|
+
return Cursor.new(row, col + offset).clamp(buffer) unless end_of_line?(buffer)
|
25
|
+
return self if final_line?(buffer)
|
26
|
+
Cursor.new(row + 1, 0)
|
27
|
+
end
|
28
|
+
|
29
|
+
def clamp(buffer)
|
30
|
+
row = @row.clamp(0, buffer.lines_count - 1)
|
31
|
+
# FIXME: Is this `buffer.lines` check needed? It ought not to be.
|
32
|
+
if !buffer.lines(row)
|
33
|
+
col = 0
|
34
|
+
else
|
35
|
+
col = @col.clamp(0, buffer.line_length(row))
|
36
|
+
end
|
37
|
+
Cursor.new(row,col)
|
38
|
+
end
|
39
|
+
|
40
|
+
def enter(buffer)
|
41
|
+
down(buffer).line_home
|
42
|
+
end
|
43
|
+
|
44
|
+
def line_home
|
45
|
+
Cursor.new(row, 0)
|
46
|
+
end
|
47
|
+
|
48
|
+
def line_end(buffer)
|
49
|
+
Cursor.new(row, buffer.line_length(row))
|
50
|
+
end
|
51
|
+
|
52
|
+
def end_of_line?(buffer)
|
53
|
+
if buffer.lines(row)
|
54
|
+
col == buffer.line_length(row)
|
55
|
+
else
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def final_line?(buffer)
|
61
|
+
row == buffer.lines_count - 1
|
62
|
+
end
|
63
|
+
|
64
|
+
def end_of_file?(buffer)
|
65
|
+
final_line?(buffer) && end_of_line?(buffer)
|
66
|
+
end
|
67
|
+
|
68
|
+
def beginning_of_file?
|
69
|
+
row == 0 && col == 0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
module EditorCore
|
3
|
+
class History
|
4
|
+
def initialize
|
5
|
+
@snapshots = []
|
6
|
+
@current = -1
|
7
|
+
end
|
8
|
+
|
9
|
+
def save(data, advance = true)
|
10
|
+
snapshots[@current+1] = data
|
11
|
+
@current += 1 if advance
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_undo?
|
15
|
+
!undo_snapshot.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def undo
|
19
|
+
undo_snapshot.tap { @current -= 1 }
|
20
|
+
end
|
21
|
+
|
22
|
+
def can_redo?
|
23
|
+
!redo_snapshot.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def redo
|
27
|
+
redo_snapshot.tap { @current += 1 }
|
28
|
+
end
|
29
|
+
|
30
|
+
def undo_snapshot
|
31
|
+
snapshots[current] if current >= 0
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :snapshots, :current
|
37
|
+
|
38
|
+
def redo_snapshot
|
39
|
+
snapshots[current + 2]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/editor_core/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: editor_core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vidar Hokstad
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-11-
|
11
|
+
date: 2021-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -28,6 +28,10 @@ files:
|
|
28
28
|
- bin/setup
|
29
29
|
- editor_core.gemspec
|
30
30
|
- lib/editor_core.rb
|
31
|
+
- lib/editor_core/buffer.rb
|
32
|
+
- lib/editor_core/core.rb
|
33
|
+
- lib/editor_core/cursor.rb
|
34
|
+
- lib/editor_core/history.rb
|
31
35
|
- lib/editor_core/version.rb
|
32
36
|
homepage: https://github.com/vidarh/editor_core
|
33
37
|
licenses:
|