text_editor 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +33 -0
  5. data/.ruby-version +1 -0
  6. data/.text_editor.rb +9 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +58 -0
  9. data/LICENSE +20 -0
  10. data/README.org +29 -0
  11. data/bin/console +9 -0
  12. data/bin/te +3 -0
  13. data/lib/core_ext/hash.rb +12 -0
  14. data/lib/core_ext/pathname.rb +9 -0
  15. data/lib/text_editor/buffer.rb +85 -0
  16. data/lib/text_editor/clipboard/cygwin.rb +20 -0
  17. data/lib/text_editor/clipboard/linux.rb +17 -0
  18. data/lib/text_editor/clipboard/mac.rb +18 -0
  19. data/lib/text_editor/clipboard/unknown.rb +17 -0
  20. data/lib/text_editor/clipboard/windows.rb +17 -0
  21. data/lib/text_editor/clipboard.rb +18 -0
  22. data/lib/text_editor/command/backspace.rb +26 -0
  23. data/lib/text_editor/command/cursor_down.rb +14 -0
  24. data/lib/text_editor/command/cursor_end.rb +10 -0
  25. data/lib/text_editor/command/cursor_left.rb +16 -0
  26. data/lib/text_editor/command/cursor_right.rb +19 -0
  27. data/lib/text_editor/command/cursor_start.rb +10 -0
  28. data/lib/text_editor/command/cursor_up.rb +13 -0
  29. data/lib/text_editor/command/insert_char.rb +11 -0
  30. data/lib/text_editor/command/insert_line.rb +16 -0
  31. data/lib/text_editor/command/insert_tab.rb +9 -0
  32. data/lib/text_editor/command/quit.rb +9 -0
  33. data/lib/text_editor/command/resolver.rb +36 -0
  34. data/lib/text_editor/command/write_file.rb +13 -0
  35. data/lib/text_editor/command.rb +26 -0
  36. data/lib/text_editor/component.rb +13 -0
  37. data/lib/text_editor/configuration/bootstrap.rb +43 -0
  38. data/lib/text_editor/configuration/dsl.rb +11 -0
  39. data/lib/text_editor/configuration/resolver.rb +45 -0
  40. data/lib/text_editor/configuration.rb +15 -0
  41. data/lib/text_editor/cursor.rb +42 -0
  42. data/lib/text_editor/keyboard.rb +54 -0
  43. data/lib/text_editor/mode/nano.rb +30 -0
  44. data/lib/text_editor/mode.rb +23 -0
  45. data/lib/text_editor/operating_system.rb +19 -0
  46. data/lib/text_editor/reactor.rb +38 -0
  47. data/lib/text_editor/state.rb +10 -0
  48. data/lib/text_editor/terminal.rb +57 -0
  49. data/lib/text_editor/window.rb +44 -0
  50. data/lib/text_editor.rb +121 -0
  51. data/log/.keep +0 -0
  52. data/spec/core_ext/hash_spec.rb +15 -0
  53. data/spec/core_ext/pathname_spec.rb +26 -0
  54. data/spec/spec_helper.rb +15 -0
  55. data/spec/text_editor/buffer_spec.rb +181 -0
  56. data/spec/text_editor/clipboard_spec.rb +28 -0
  57. data/spec/text_editor/command/quit_spec.rb +14 -0
  58. data/spec/text_editor/command/resolver_spec.rb +56 -0
  59. data/spec/text_editor/command_spec.rb +85 -0
  60. data/spec/text_editor/component_spec.rb +40 -0
  61. data/spec/text_editor/configuration_spec.rb +25 -0
  62. data/spec/text_editor/cursor_spec.rb +93 -0
  63. data/spec/text_editor/operating_system_spec.rb +55 -0
  64. data/spec/text_editor/reactor_spec.rb +34 -0
  65. data/spec/text_editor/state_spec.rb +20 -0
  66. data/text_editor.gemspec +18 -0
  67. metadata +143 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1b869a9294584c550357a869d6d61efbda2d7e9a
4
+ data.tar.gz: 1eafb0f6ffa81a53b43f5b74759e0e5354ba688e
5
+ SHA512:
6
+ metadata.gz: 9c10910e79d481967bde183696d1dfe89a973b8a244d3ee36a38b46abb7769147aad71c84b56ee7153d27288d112eb271bec745c09f24dd5b71dfd2f184ffd7d
7
+ data.tar.gz: e3f686ec346c8e65c27a51d402bfef9164f7926e6396c30ce129149d8b332969afb0d6c20ccd17dca7e3b8883930e7b05c0b0a2f4cb4e470affb633a77424687
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ log
2
+ tmp
3
+ vendor/bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ AllCops:
2
+ DisplayCopNames: true
3
+ TargetRubyVersion: 2.3
4
+
5
+ Metrics/AbcSize:
6
+ Enabled: false
7
+
8
+ Style/CaseEquality:
9
+ Enabled: false
10
+
11
+ Metrics/CyclomaticComplexity:
12
+ Enabled: false
13
+
14
+ Metrics/MethodLength:
15
+ Enabled: false
16
+
17
+ Style/Documentation:
18
+ Enabled: false
19
+
20
+ Style/DoubleNegation:
21
+ Enabled: false
22
+
23
+ Style/FrozenStringLiteralComment:
24
+ Enabled: false
25
+
26
+ Style/SafeNavigation:
27
+ Enabled: false
28
+
29
+ Style/StringLiterals:
30
+ EnforcedStyle: double_quotes
31
+
32
+ Style/TrailingCommaInLiteral:
33
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.text_editor.rb ADDED
@@ -0,0 +1,9 @@
1
+ TextEditor.configure do
2
+ log.file = nil
3
+ log.level = Logger::INFO
4
+ log.options = []
5
+
6
+ tab.expand = true
7
+ tab.soft = true
8
+ tab.width = 2
9
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ gem "codeclimate-test-reporter"
5
+ gem "rspec"
6
+ gem "rubocop"
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ text_editor (0.0.0)
5
+ curses (= 1.0.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.3.0)
11
+ codeclimate-test-reporter (0.6.0)
12
+ simplecov (>= 0.7.1, < 1.0.0)
13
+ curses (1.0.2)
14
+ diff-lcs (1.2.5)
15
+ docile (1.1.5)
16
+ json (2.0.2)
17
+ parser (2.3.1.4)
18
+ ast (~> 2.2)
19
+ powerpack (0.1.1)
20
+ rainbow (2.1.0)
21
+ rspec (3.5.0)
22
+ rspec-core (~> 3.5.0)
23
+ rspec-expectations (~> 3.5.0)
24
+ rspec-mocks (~> 3.5.0)
25
+ rspec-core (3.5.3)
26
+ rspec-support (~> 3.5.0)
27
+ rspec-expectations (3.5.0)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.5.0)
30
+ rspec-mocks (3.5.0)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.5.0)
33
+ rspec-support (3.5.0)
34
+ rubocop (0.43.0)
35
+ parser (>= 2.3.1.1, < 3.0)
36
+ powerpack (~> 0.1)
37
+ rainbow (>= 1.99.1, < 3.0)
38
+ ruby-progressbar (~> 1.7)
39
+ unicode-display_width (~> 1.0, >= 1.0.1)
40
+ ruby-progressbar (1.8.1)
41
+ simplecov (0.12.0)
42
+ docile (~> 1.1.0)
43
+ json (>= 1.8, < 3)
44
+ simplecov-html (~> 0.10.0)
45
+ simplecov-html (0.10.0)
46
+ unicode-display_width (1.1.1)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ codeclimate-test-reporter
53
+ rspec
54
+ rubocop
55
+ text_editor!
56
+
57
+ BUNDLED WITH
58
+ 1.12.5
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Sean Huber - sean@shuber.io
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.org ADDED
@@ -0,0 +1,29 @@
1
+ * Text Editor
2
+ Text editor written in [[https://www.ruby-lang.org/en/][Ruby]] and heavily inspired by [[http://www.vim.org/][vim]], [[https://www.gnu.org/software/emacs/][emacs]], [[https://www.nano-editor.org/][nano]], and [[https://github.com/grosser/ruco][ruco]].
3
+ ** API
4
+ *** Buffer
5
+ - An in-memory text representation of a file
6
+ - Does not overwrite the file until the buffer is saved
7
+ *** Component
8
+ - Convenience methods for working with =TextEditor= objects
9
+ *** Configuration
10
+ - Manages misc editor configuration
11
+ *** Cursor
12
+ - A cursor instance specific to an individual =Window=
13
+ *** Keyboard
14
+ - Listens for keyboard input
15
+ - Translates raw input into character symbols e.g. =:escape=
16
+ *** Window
17
+ - A viewport of a specific buffer
18
+ - Multiple windows may reference the same buffer
19
+ - Windows have their own settings and viewport or cursor positions
20
+ - Windows have three possible states: active, hidden, inactive
21
+ *** Tab
22
+ - A collection of windows
23
+ - Only one tab is visible at a time
24
+ *** Terminal
25
+ - Responsible for interacting with the console
26
+ *** TextEditor
27
+ - Dependency injection container
28
+ ** Inspiration
29
+ https://github.com/showcases/text-editors
data/bin/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/text_editor"
3
+
4
+ def editor
5
+ @editor ||= TextEditor.new(ARGV)
6
+ end
7
+
8
+ require "irb"
9
+ IRB.start
data/bin/te ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/text_editor"
3
+ TextEditor.new(ARGV).start
@@ -0,0 +1,12 @@
1
+ module CoreExt
2
+ module Hash
3
+ refine ::Hash do
4
+ def transform_keys
5
+ reduce({}) do |hash, (key, value)|
6
+ key = yield(key)
7
+ hash.update(key => value)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module CoreExt
2
+ module Pathname
3
+ refine ::Pathname do
4
+ def safe_read
5
+ read if exist?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,85 @@
1
+ class TextEditor
2
+ class Buffer
3
+ using CoreExt::Pathname
4
+
5
+ def initialize(lines = nil)
6
+ @lines = parse_lines(lines)
7
+ end
8
+
9
+ def delete_char(line, column)
10
+ validate_line!(line)
11
+ content = self.line(line)
12
+ content.slice!(column)
13
+ replace_line(line, content)
14
+ end
15
+
16
+ def delete_line(line)
17
+ lines = @lines.dup
18
+ lines.delete_at(line)
19
+ self.class.new(lines)
20
+ end
21
+
22
+ def each_line(&block)
23
+ @lines.each(&block)
24
+ end
25
+
26
+ def insert_char(line, column, char)
27
+ validate_line!(line)
28
+ content = self.line(line)
29
+ content.insert(column, char)
30
+ replace_line(line, content)
31
+ end
32
+
33
+ def insert_line(line, content = "")
34
+ lines = @lines.dup.insert(line, content)
35
+ self.class.new(lines.compact)
36
+ end
37
+
38
+ def line(line)
39
+ @lines[line].to_s.dup
40
+ end
41
+
42
+ def replace_char(line, column, char)
43
+ validate_line!(line)
44
+ content = self.line(line)
45
+ content[column] = char
46
+ replace_line(line, content)
47
+ end
48
+
49
+ def replace_line(line, content = "")
50
+ validate_line!(line)
51
+ lines = @lines.dup
52
+ lines[line] = content
53
+ self.class.new(lines.compact)
54
+ end
55
+
56
+ def size
57
+ @lines.size
58
+ end
59
+
60
+ def to_s
61
+ @lines.join("\n")
62
+ end
63
+
64
+ private
65
+
66
+ def parse_lines(lines)
67
+ case lines
68
+ when nil, ""
69
+ [""]
70
+ when Array
71
+ lines
72
+ when Pathname
73
+ parse_lines(lines.safe_read)
74
+ when String
75
+ lines.each_line.map(&:chomp)
76
+ else
77
+ raise ArgumentError
78
+ end
79
+ end
80
+
81
+ def validate_line!(line)
82
+ raise IndexError if line >= size
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ class Cygwin
4
+ CLIPBOARD = "/dev/clipboard".freeze
5
+
6
+ def clear
7
+ copy("")
8
+ end
9
+
10
+ def copy(content)
11
+ File.write(CLIPBOARD, content)
12
+ content
13
+ end
14
+
15
+ def paste
16
+ File.read(CLIPBOARD)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ class Linux
4
+ def clear
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def copy(_content)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def paste
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ class Mac
4
+ def clear
5
+ copy("")
6
+ end
7
+
8
+ def copy(content)
9
+ system("printf #{content.inspect} | pbcopy")
10
+ content
11
+ end
12
+
13
+ def paste
14
+ `pbpaste`
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ class Unknown
4
+ def clear
5
+ copy("")
6
+ end
7
+
8
+ def copy(content)
9
+ @clipboard = content
10
+ end
11
+
12
+ def paste
13
+ @clipboard.to_s
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ class Windows
4
+ def clear
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def copy(_content)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def paste
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class TextEditor
2
+ class Clipboard
3
+ extend Forwardable
4
+ include Component
5
+
6
+ def_delegators :adapter, :clear, :copy, :paste
7
+
8
+ private
9
+
10
+ def adapter
11
+ @adapter ||= type.new
12
+ end
13
+
14
+ def type
15
+ self.class.const_get(editor.os.type)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ class TextEditor
2
+ class Command
3
+ class Backspace < Command
4
+ def run(*)
5
+ if column.zero? && line.nonzero?
6
+ content = buffer.line(line.pred)
7
+ new_column = content.size
8
+ content << buffer.line(line)
9
+
10
+ new_buffer = buffer.replace_line(line.pred, content)
11
+ new_buffer = new_buffer.delete_line(line)
12
+
13
+ new_cursor = cursor.move(line.pred, new_column)
14
+ elsif column.nonzero?
15
+ new_buffer = buffer.delete_char(line, column.pred)
16
+ new_cursor = cursor.left
17
+ else
18
+ new_buffer = buffer
19
+ new_cursor = cursor
20
+ end
21
+
22
+ window.update(new_buffer, new_cursor)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorDown < Command
4
+ def run(*)
5
+ lines = buffer.size.pred
6
+ return if line == lines
7
+
8
+ columns = buffer.line(line.next).size
9
+ new_cursor = cursor.down.clamp(lines, columns)
10
+ window.update(buffer, new_cursor)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorEnd < Command
4
+ def run(*)
5
+ new_cursor = cursor.move(line, content.size)
6
+ window.update(buffer, new_cursor)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorLeft < Command
4
+ def run(*)
5
+ if column.zero? && line.nonzero?
6
+ column = buffer.line(line.pred).size
7
+ new_cursor = cursor.move(line.pred, column)
8
+ else
9
+ new_cursor = cursor.left
10
+ end
11
+
12
+ window.update(buffer, new_cursor)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorRight < Command
4
+ def run(*)
5
+ columns = content.size
6
+ lines = buffer.size.pred
7
+
8
+ new_cursor =
9
+ if column == columns && line != lines
10
+ cursor.move(line.next, 0)
11
+ else
12
+ cursor.right.clamp(lines, columns)
13
+ end
14
+
15
+ window.update(buffer, new_cursor)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorStart < Command
4
+ def run(*)
5
+ new_cursor = cursor.move(line, 0)
6
+ window.update(buffer, new_cursor)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ class TextEditor
2
+ class Command
3
+ class CursorUp < Command
4
+ def run(*)
5
+ return unless line.zero?
6
+
7
+ columns = buffer.line(line.pred).size
8
+ new_cursor = cursor.up.clamp(line.pred, columns)
9
+ window.update(buffer, new_cursor)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class TextEditor
2
+ class Command
3
+ class InsertChar < Command
4
+ def run(key)
5
+ new_buffer = buffer.insert_char(line, column, key)
6
+ new_cursor = cursor.right(key.size)
7
+ window.update(new_buffer, new_cursor)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ class TextEditor
2
+ class Command
3
+ class InsertLine < Command
4
+ def run(*)
5
+ current = content[0...column]
6
+ newline = content[column..-1]
7
+
8
+ new_buffer = buffer.replace_line(line, current)
9
+ new_buffer = new_buffer.insert_line(line.next, newline)
10
+
11
+ new_cursor = cursor.move(line.next, 0)
12
+ window.update(new_buffer, new_cursor)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ class TextEditor
2
+ class Command
3
+ class InsertTab < Command
4
+ def run(*)
5
+ command.insert_char(" ")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class TextEditor
2
+ class Command
3
+ class Quit < Command
4
+ def run(*)
5
+ editor.reactor.stop
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ class TextEditor
2
+ class Command
3
+ class Resolver
4
+ include Component
5
+
6
+ def method_missing(method, *args, &block)
7
+ command = resolve(method)
8
+ return super unless command
9
+ command.run(*args, &block)
10
+ self
11
+ end
12
+
13
+ def resolve(method)
14
+ parts = method.to_s.split("_")
15
+ name = parts.map(&:capitalize).join
16
+ command(name)
17
+ end
18
+
19
+ private
20
+
21
+ def command(name)
22
+ namespace.const_get(name).new(editor)
23
+ rescue NameError
24
+ nil
25
+ end
26
+
27
+ def namespace
28
+ Module.nesting[1]
29
+ end
30
+
31
+ def respond_to_missing?(method, include_super)
32
+ resolve(method) || super
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ class TextEditor
2
+ class Command
3
+ class WriteFile < Command
4
+ def run(*)
5
+ if window.file
6
+ window.file.write(buffer.to_s)
7
+ else
8
+ terminal.beep
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ class TextEditor
2
+ class Command
3
+ extend Forwardable
4
+ include Component
5
+
6
+ def_delegators :editor, :window
7
+ def_delegators :window, :buffer, :cursor, :state
8
+ def_delegators :cursor, :line, :column
9
+
10
+ def self.call(editor, *args, &block)
11
+ new(editor).run(*args, &block)
12
+ end
13
+
14
+ def command
15
+ @command ||= Resolver.new(editor)
16
+ end
17
+
18
+ def content
19
+ buffer.line(line)
20
+ end
21
+
22
+ def run(*)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end