text_editor 0.0.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.
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