gloo 7.1.3 → 7.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +23 -0
  3. data/docs/application.md +47 -2
  4. data/docs/development.md +10 -0
  5. data/docs/language_objects.md +20 -15
  6. data/docs/language_scripting.md +6 -2
  7. data/docs/testing.md +4 -7
  8. data/docs/verbs.md +23 -3
  9. data/lib/VERSION +1 -1
  10. data/lib/VERSION_NOTES +8 -0
  11. data/lib/dependencies.rb +4 -1
  12. data/lib/gloo/app/engine.rb +18 -6
  13. data/lib/gloo/core/obj.rb +0 -23
  14. data/lib/gloo/objs/basic/script.rb +0 -23
  15. data/lib/gloo/persist/declaration_ledger.rb +9 -9
  16. data/lib/gloo/persist/declaration_writer.rb +208 -0
  17. data/lib/gloo/persist/disc_mech.rb +70 -39
  18. data/lib/gloo/persist/file_loader.rb +125 -66
  19. data/lib/gloo/persist/file_saver.rb +130 -198
  20. data/lib/gloo/persist/file_storage.rb +15 -4
  21. data/lib/gloo/persist/file_style.rb +89 -0
  22. data/lib/gloo/persist/line_splitter.rb +16 -1
  23. data/lib/gloo/persist/persist_man.rb +112 -30
  24. data/lib/gloo/persist/save_index.rb +211 -0
  25. data/lib/gloo/persist/script_body_collector.rb +16 -21
  26. data/lib/gloo/persist/source/blank_node.rb +10 -3
  27. data/lib/gloo/persist/source/comment_node.rb +10 -2
  28. data/lib/gloo/persist/source/directive_node.rb +10 -2
  29. data/lib/gloo/persist/source/obj_node.rb +174 -9
  30. data/lib/gloo/persist/source/source_doc.rb +54 -0
  31. data/lib/gloo/persist/subtree_extractor.rb +9 -37
  32. data/lib/gloo/persist/trivia_buffer.rb +7 -7
  33. data/lib/gloo/verbs/load.rb +27 -21
  34. data/lib/gloo/verbs/move.rb +42 -3
  35. data/lib/gloo/verbs/save.rb +26 -4
  36. data/test.gloo/app/exit_status.test.gloo +34 -0
  37. data/test.gloo/app/fixtures/exit_after_error.gloo +9 -0
  38. data/test.gloo/lang/declaration.test.gloo +36 -0
  39. data/test.gloo/lang/fixtures/mixed_indent.gloo +5 -0
  40. data/test.gloo/lang/naming.test.gloo +44 -6
  41. data/test.gloo/verbs/fixtures/load_beside.gloo +6 -0
  42. data/test.gloo/verbs/fixtures/lossless_text.gloo +7 -0
  43. data/test.gloo/verbs/fixtures/lossless_text_saved.gloo +9 -0
  44. data/test.gloo/verbs/load.test.gloo +18 -0
  45. data/test.gloo/verbs/move.test.gloo +39 -0
  46. data/test.gloo/verbs/save.test.gloo +239 -0
  47. metadata +10 -1
@@ -3,10 +3,10 @@
3
3
  #
4
4
  # One object declaration in a source file, eg.
5
5
  # name [type] : value
6
- # Carries the raw text needed to reproduce the declaration unchanged,
7
- # plus a back-reference to the heap object it created (or updated),
8
- # so a save can tell whether the live value still matches what's on
9
- # disk and, if so, leave the raw text alone.
6
+ # Carries the declaration's exact source text, plus a back-reference
7
+ # to the heap object it created (or named again), so a save can tell
8
+ # whether the object is still there and its value unchanged and, if
9
+ # so, write the text back exactly as it was read.
10
10
  #
11
11
 
12
12
  module Gloo
@@ -19,8 +19,23 @@ module Gloo
19
19
  # :begin_end -- name [type] : BEGIN ... END
20
20
  # :body -- name [type] :, followed by an indented,
21
21
  # unparsed body (script commands)
22
+ #
23
+ # raw is the declaration's own exact text: its line (with any
24
+ # continuation lines), and its BEGIN/END block or script body,
25
+ # line endings included -- not its leading_doc or children.
26
+ # head is the declaration line up to its value (indent, name and
27
+ # [type], as written), raw_tail what follows it.
28
+ #
29
+ # repaired_head is set when gloo had to guess at the head (a
30
+ # missing name or ']'): a save writes the head as understood.
31
+ #
32
+ # home is the object's parent and snapshot a copy of its value,
33
+ # both taken when the file finished loading: the object is still
34
+ # in its place if its parent is home, and unchanged if its value
35
+ # equals the snapshot.
22
36
  attr_accessor :name, :raw_type, :raw_indent, :raw_tail, :block_style,
23
- :raw_value, :raw_end_indent, :leading_doc, :trailing_comment, :obj
37
+ :raw_value, :leading_doc, :obj, :raw, :head,
38
+ :home, :snapshot, :repaired_head
24
39
  attr_reader :children
25
40
 
26
41
  #
@@ -35,15 +50,80 @@ module Gloo
35
50
  @raw_tail = ''
36
51
  @block_style = :inline
37
52
  @raw_value = nil
38
- # a begin_end block's closing END is not necessarily indented
39
- # to match its declaration -- kept separately, raw.
40
- @raw_end_indent = ''
41
53
  @leading_doc = nil
42
- @trailing_comment = nil
43
54
  @obj = nil
55
+ @raw = ''
56
+ @head = ''
57
+ @home = nil
58
+ @snapshot = nil
59
+ @repaired_head = nil
44
60
  @children = []
45
61
  end
46
62
 
63
+ #
64
+ # The exact source text of the declaration, its leading_doc and
65
+ # everything under it, as read.
66
+ #
67
+ def text
68
+ str = "#{@leading_doc}#{@raw}"
69
+ @children.each { |child| str << child.text }
70
+ return str
71
+ end
72
+
73
+ #
74
+ # A copy of this declaration with no children, and with the
75
+ # given text in place of its own (for a save's new document;
76
+ # this node is left as it is).
77
+ #
78
+ def detached( raw = @raw )
79
+ copy = dup
80
+ copy.raw = raw
81
+ copy.reset_children
82
+ return copy
83
+ end
84
+
85
+ #
86
+ # A copy with the head gloo understood in place of the one
87
+ # written (see repaired_head); this node if nothing was repaired.
88
+ #
89
+ def repaired
90
+ return self unless @repaired_head
91
+
92
+ copy = detached( @raw.sub( @head, @repaired_head ) )
93
+ copy.head = @repaired_head
94
+ copy.repaired_head = nil
95
+ return copy
96
+ end
97
+
98
+ #
99
+ # Record where the object is and what its value is, now that the
100
+ # file has finished loading.
101
+ #
102
+ def remember
103
+ return unless @obj
104
+
105
+ @home = @obj.parent
106
+ @snapshot = copy_of( @obj.value )
107
+ end
108
+
109
+ #
110
+ # Is the object still under the parent it was loaded under?
111
+ #
112
+ def in_place?
113
+ return false unless @obj && @home
114
+
115
+ return @obj.parent.equal?( @home ) && @home.children.any? { |c| c.equal?( @obj ) }
116
+ end
117
+
118
+ #
119
+ # Has the object's value changed since the file was loaded?
120
+ #
121
+ def changed?
122
+ return false unless @obj
123
+
124
+ return @obj.value != @snapshot
125
+ end
126
+
47
127
  #
48
128
  # The cleaned-up leading_doc: each line's leading whitespace and
49
129
  # its '#' marker (plus one following space, if any) stripped,
@@ -59,8 +139,93 @@ module Gloo
59
139
  return dedent( lines ).join( "\n" )
60
140
  end
61
141
 
142
+ #
143
+ # Give the declaration a new name as written (eg. a dotted path
144
+ # when it moves into a file of its own). Only the name on its
145
+ # first line changes.
146
+ #
147
+ def rename( new_name )
148
+ @raw = @raw.sub( /\A([ \t]*)#{Regexp.escape( @name )}/ ) { "#{$1}#{new_name}" }
149
+ @head = @head.sub( /\A([ \t]*)#{Regexp.escape( @name )}/ ) { "#{$1}#{new_name}" }
150
+ @name = new_name
151
+ end
152
+
153
+ #
154
+ # Move the declaration (and everything under it) to a new
155
+ # indentation. Its own lines and its script body shift by the
156
+ # same amount; a BEGIN/END block's content is the value, so it
157
+ # stays exactly as it is, and END lines up with the declaration.
158
+ # A leading_doc's lines are set to the new indentation.
159
+ #
160
+ def reindent( new_indent )
161
+ old = @raw_indent
162
+ @leading_doc = reindent_doc( @leading_doc, new_indent ) if @leading_doc
163
+ @raw = reindent_own( @raw, old, new_indent )
164
+ @head = shift( @head, old, new_indent )
165
+ @raw_indent = new_indent
166
+ @children.each do |child|
167
+ next unless child.is_a?( ObjNode )
168
+
169
+ child.reindent( shift( child.raw_indent, old, new_indent ) )
170
+ end
171
+ end
172
+
173
+ protected
174
+
175
+ #
176
+ # Start with no children (for a detached copy).
177
+ #
178
+ def reset_children
179
+ @children = []
180
+ end
181
+
62
182
  private
63
183
 
184
+ #
185
+ # A copy of a value that can't be changed through the original
186
+ # (a script's lines are an Array that grows in place).
187
+ #
188
+ def copy_of( value )
189
+ return Marshal.load( Marshal.dump( value ) )
190
+ rescue TypeError
191
+ return value.dup
192
+ end
193
+
194
+ #
195
+ # The declaration's own lines at a new indentation.
196
+ #
197
+ def reindent_own( text, old, new_indent )
198
+ lines = text.lines
199
+ return text if lines.empty?
200
+
201
+ if @block_style == :begin_end
202
+ ending = lines.last[ /\r?\n\z/ ].to_s
203
+ lines[ 0 ] = shift( lines[ 0 ], old, new_indent )
204
+ lines[ -1 ] = "#{new_indent}END#{ending}" if lines.size > 1
205
+ return lines.join
206
+ end
207
+
208
+ return lines.map { |l| l.strip.empty? ? l : shift( l, old, new_indent ) }.join
209
+ end
210
+
211
+ #
212
+ # Replace a line's old indentation prefix with the new one; a
213
+ # line that doesn't start with it gets the new one added.
214
+ #
215
+ def shift( line, old, new_indent )
216
+ return "#{new_indent}#{line.delete_prefix( old )}" if line.start_with?( old )
217
+
218
+ return "#{new_indent}#{line.lstrip}"
219
+ end
220
+
221
+ #
222
+ # Re-indent a leading_doc's lines to new_indent, replacing
223
+ # whatever leading whitespace each '#' line had.
224
+ #
225
+ def reindent_doc( raw, new_indent )
226
+ return raw.lines.map { |line| "#{new_indent}#{line.lstrip}" }.join
227
+ end
228
+
64
229
  #
65
230
  # One raw comment line -> its text past the '#': leading
66
231
  # whitespace dropped, then the '#' and at most one space after
@@ -19,11 +19,53 @@ module Gloo
19
19
 
20
20
  attr_reader :children
21
21
 
22
+ # A file whose last line has no line ending: every node's text
23
+ # ends with one anyway (so new text can follow any node), and
24
+ # the document leaves the last one off.
25
+ attr_accessor :no_final_newline
26
+
22
27
  #
23
28
  # Set up an empty source document.
24
29
  #
25
30
  def initialize
26
31
  @children = []
32
+ @no_final_newline = false
33
+ end
34
+
35
+ #
36
+ # The file's exact text: every node's text, in order. Right
37
+ # after a load this is the file, byte for byte.
38
+ #
39
+ def text
40
+ str = @children.map( &:text ).join
41
+ str = str.delete_suffix( "\n" ) if @no_final_newline
42
+ return str
43
+ end
44
+
45
+ #
46
+ # Note that the file's last line had no line ending, and give it
47
+ # one in the node that holds it (see no_final_newline).
48
+ #
49
+ def missing_final_newline!
50
+ leaf = last_text_node( @children )
51
+ return unless leaf
52
+
53
+ leaf.raw << "\n"
54
+ @no_final_newline = true
55
+ end
56
+
57
+ #
58
+ # Every object declaration in the document, at any depth, in
59
+ # file order.
60
+ #
61
+ def obj_nodes( nodes = @children, found = [] )
62
+ nodes.each do |node|
63
+ next unless node.is_a?( Source::ObjNode )
64
+
65
+ found << node
66
+ obj_nodes( node.children, found )
67
+ end
68
+ return found
27
69
  end
28
70
 
29
71
  #
@@ -51,6 +93,18 @@ module Gloo
51
93
 
52
94
  private
53
95
 
96
+ #
97
+ # The node whose text comes last in the file: the last node, or
98
+ # its last child (at any depth) if it has children.
99
+ #
100
+ def last_text_node( nodes )
101
+ last = nodes.last
102
+ return nil unless last
103
+ return last unless last.is_a?( Source::ObjNode ) && last.children.any?
104
+
105
+ return last_text_node( last.children )
106
+ end
107
+
54
108
  #
55
109
  # Depth-first search of nodes (and their children) for the one
56
110
  # linked to obj; removes it from whichever children array it's
@@ -46,11 +46,13 @@ module Gloo
46
46
  return err( SPANS_MULTIPLE_FILES_ERR ) if owners.count > 1
47
47
 
48
48
  source_fs = owners.first
49
- node = source_fs ? remove_node( obj, source_fs ) : fresh_node( obj )
49
+ return [ fresh_node( obj ), nil ] unless source_fs
50
+
51
+ node = remove_node( obj, source_fs )
50
52
  return err( NO_OWN_DECLARATION_ERR ) unless node
51
53
 
52
- node.name = dotted_name( obj )
53
- reindent( node, 0 )
54
+ node.rename( dotted_name( obj ) )
55
+ node.reindent( '' )
54
56
  return [ node, source_fs ]
55
57
  end
56
58
 
@@ -110,13 +112,12 @@ module Gloo
110
112
 
111
113
  #
112
114
  # obj has no file of its own (never loaded, or created at
113
- # runtime since) -- build a plain node with no raw text of its
114
- # own. FileSaver renders it fresh from the live object, the same
115
- # way it already does for any brand-new child.
115
+ # runtime since) -- write it, and everything under it, from
116
+ # scratch, named by its dotted path.
116
117
  #
117
118
  def fresh_node( obj )
118
- node = Source::ObjNode.new( :name => obj.name, :raw_type => obj.type_display )
119
- node.obj = obj
119
+ node = FileSaver.new( @engine, nil, obj ).new_node( obj, dotted_name( obj ) )
120
+ Source::SourceDoc.new.obj_nodes( [ node ] ).each( &:remember )
120
121
  return node
121
122
  end
122
123
 
@@ -150,35 +151,6 @@ module Gloo
150
151
  return names.join( '.' )
151
152
  end
152
153
 
153
- #
154
- # Recompute raw_indent (and, for a BEGIN/END node, raw_end_indent
155
- # to match) from scratch for the moved node and every descendant
156
- # ObjNode. Their old indentation depth described nesting inside
157
- # the file they came from; here obj itself is depth 0, in a
158
- # brand-new file. Floating trivia (a blank line or comment not
159
- # attached as some declaration's leading_doc) travels along
160
- # unindented-adjusted -- purely cosmetic, since indentation never
161
- # affects how a comment/blank line is recognized on reload.
162
- #
163
- def reindent( node, depth )
164
- new_indent = "\t" * depth
165
- node.raw_indent = new_indent
166
- node.raw_end_indent = new_indent if node.block_style == :begin_end
167
- node.leading_doc = reindent_doc( node.leading_doc, new_indent ) if node.leading_doc
168
- node.children.each do |child|
169
- reindent( child, depth + 1 ) if child.is_a?( Source::ObjNode )
170
- end
171
- end
172
-
173
- #
174
- # Re-indent a raw leading_doc's lines to new_indent, replacing
175
- # whatever leading whitespace each '#'-prefixed line had in the
176
- # file it came from.
177
- #
178
- def reindent_doc( raw, new_indent )
179
- return raw.split( "\n" ).map { |line| "#{new_indent}#{line.lstrip}" }.join( "\n" )
180
- end
181
-
182
154
  end
183
155
  end
184
156
  end
@@ -29,16 +29,16 @@ module Gloo
29
29
  end
30
30
 
31
31
  #
32
- # Buffer one comment line. raw is the full source line (minus
33
- # its trailing newline); tabs is its indentation level.
32
+ # Buffer one comment line. raw is the full source line, with its
33
+ # line ending; tabs is its indentation level.
34
34
  #
35
35
  def push_comment( raw, tabs )
36
36
  @pending << { :kind => :comment, :raw => raw, :tabs => tabs }
37
37
  end
38
38
 
39
39
  #
40
- # Buffer one blank line. raw is its exact source text (may be
41
- # non-empty if it had trailing whitespace) -- a blank line always
40
+ # Buffer one blank line. raw is its exact source text, with its
41
+ # line ending and any trailing whitespace -- a blank line always
42
42
  # breaks a comment run's association with whatever declaration
43
43
  # follows, the same as it always has.
44
44
  #
@@ -61,8 +61,8 @@ module Gloo
61
61
  #
62
62
  # If the run since the last blank (or the whole run, if it has no
63
63
  # blank in it) is one or more comment lines all at the same
64
- # indent as line_tabs, claim it as a leading_doc (raw,
65
- # newline-joined). Anything before that -- an earlier blank, and
64
+ # indent as line_tabs, claim it as a leading_doc (the raw lines,
65
+ # line endings included). Anything before that -- an earlier blank, and
66
66
  # whatever preceded it -- floats into children first, in order,
67
67
  # since a blank always breaks the association. If the trailing
68
68
  # run doesn't qualify (wrong indent, or empty), it floats into
@@ -74,7 +74,7 @@ module Gloo
74
74
  flush_list( before, children )
75
75
 
76
76
  if after.any? && after.last[ :tabs ] == line_tabs
77
- doc = after.map { |t| t[ :raw ] }.join( "\n" )
77
+ doc = after.map { |t| t[ :raw ] }.join
78
78
  @pending = []
79
79
  return doc
80
80
  end
@@ -63,12 +63,15 @@ module Gloo
63
63
  return KEYWORD_SHORT
64
64
  end
65
65
 
66
- #
67
- # Load a gloo file
68
- #
66
+ #
67
+ # Load a gloo file. A relative name is looked for in the
68
+ # persist manager's search folders (next to this file, the
69
+ # app or projects folder, the gloo home).
70
+ #
69
71
  def load_gloo_file( fn )
70
72
  @engine.log.debug "Getting ready to load gloo file: #{fn}"
71
- @engine.persist_man.load fn
73
+ pm = @engine.persist_man
74
+ pm.load( fn, pm.search_folders )
72
75
  end
73
76
 
74
77
  #
@@ -98,16 +101,17 @@ module Gloo
98
101
  {
99
102
  :name => KEYWORD,
100
103
  :shortcut => KEYWORD_SHORT,
101
- :description => 'Load an object file. There are two ways to ' \
102
- 'specify the file. Give either the full path and file name ' \
103
- 'or use a relative path from the gloo project folder. For ' \
104
- 'the latter, the extension is not needed. For the former, ' \
105
- 'the file extension is necessary. Using * instead of a file ' \
106
- 'name will load all gloo files in the folder. When using the ' \
107
- '--app option, the file is loaded from the application ' \
108
- 'folder, but if not found there, the loader will also look ' \
109
- 'in the gloo root folder. The load verb is also used to ' \
110
- 'import an extension or library at runtime.',
104
+ :description => 'Load an object file. Give a full path, one ' \
105
+ 'starting with ~, or a relative name; the .gloo extension is ' \
106
+ 'optional. A relative name is looked for in a few places, in ' \
107
+ 'order. In App mode (--app): the app folder, then next to the ' \
108
+ 'file the loading script came from, then the gloo home. ' \
109
+ 'Otherwise: next to the loading file, then the projects ' \
110
+ 'folder (the project_path setting), then the gloo home. A ' \
111
+ 'command typed at the prompt has no file, so the working ' \
112
+ 'folder stands in for it. Using * instead of a file name ' \
113
+ 'loads all gloo files in the folder. The load verb is also ' \
114
+ 'used to import an extension or library at runtime.',
111
115
  :syntax => [
112
116
  'load {file_name}',
113
117
  'load ext {my_ext}',
@@ -116,11 +120,12 @@ module Gloo
116
120
  ],
117
121
  :parameters => [
118
122
  '{file_name} — Name of the object file that is to be loaded: ' \
119
- 'relative to the project, a full path, or starting with ~. ' \
123
+ 'a relative name (see the description for where it is ' \
124
+ 'looked for), a full path, or starting with ~. ' \
120
125
  'The .gloo extension is optional. ' \
121
- 'End it with * to load every .gloo file in a folder (a ' \
122
- 'folder in the project, a full path, or one starting with ' \
123
- '~); anything before the * is the start of the file names.',
126
+ 'End it with * to load every .gloo file in a folder, found ' \
127
+ 'the same way; the first place with matching files is ' \
128
+ 'used. Anything before the * is the start of the file names.',
124
129
  'reference type — By default, the type is file. File can be ' \
125
130
  'specified, but does not need to be. Use ext to load a ' \
126
131
  'User Extension. Use lib to load a Core Library.',
@@ -136,8 +141,8 @@ module Gloo
136
141
  "#{UNKNOWN_OPT_ERR} — The reference type given isn't file, ext, or lib.",
137
142
  "#{WRONG_NUM_ARGS_ERR} — load expects 2 or 3 arguments (the verb, an optional reference type, and the file/ext/lib name).",
138
143
  'File not found: {name} — The file name given could not be ' \
139
- 'resolved to a gloo file (checked as a full path, and ' \
140
- 'relative to the project and gloo root folders).'
144
+ 'resolved to a gloo file in any of the places it is ' \
145
+ 'looked for.'
141
146
  ],
142
147
  :notes => 'Several files can contribute to one container: ' \
143
148
  'declare it in each file and add different children, and ' \
@@ -156,7 +161,8 @@ module Gloo
156
161
  'is declared in more than one loaded file, the first ' \
157
162
  'non-empty doc wins, same as the first value.',
158
163
  :examples => <<~EXAMPLES.strip
159
- > load my/project/file
164
+ # Next to this file (or the app folder, in App mode)
165
+ > load parts/helpers
160
166
  > load my/app/*
161
167
  > load ~/.my_app/settings.gloo
162
168
  > load ~/.my_app/plugins/*
@@ -13,9 +13,15 @@ module Gloo
13
13
  TO = 'to'.freeze
14
14
  MISSING_SRC_ERR = 'Object to move was not specified!'.freeze
15
15
  MISSING_DST_ERR = "'Move' must include 'to' parent object!".freeze
16
+ NAME_TAKEN_ERR = "Could not move '%s': '%s' already has a child named '%s'.".freeze
17
+ INSIDE_ITSELF_ERR = "Could not move '%s': it can't go inside itself.".freeze
16
18
 
17
19
  #
18
20
  # Run the verb.
21
+ # A name is unique in its container, so an object can't move into
22
+ # one that already has a child with its name; nor can it move
23
+ # inside itself. Moving it to the parent it's already in does
24
+ # nothing.
19
25
  #
20
26
  def run
21
27
  dst = lookup_dst
@@ -23,6 +29,9 @@ module Gloo
23
29
 
24
30
  o = lookup_obj
25
31
  return unless o
32
+ return if dst.equal?( o.parent )
33
+ return refuse( format( INSIDE_ITSELF_ERR, o.pn ) ) if inside?( dst, o )
34
+ return refuse( format( NAME_TAKEN_ERR, o.pn, dst.pn, o.name ) ) if dst.contains_child?( o.name )
26
35
 
27
36
  o.parent.remove_child o
28
37
  dst.add_child o
@@ -48,6 +57,28 @@ module Gloo
48
57
 
49
58
  private
50
59
 
60
+ #
61
+ # Report why the object can't be moved; nothing moves, and it is
62
+ # false.
63
+ #
64
+ def refuse( msg )
65
+ @engine.err msg
66
+ @engine.heap.it.set_to false
67
+ end
68
+
69
+ #
70
+ # Is dst the object itself, or somewhere under it?
71
+ #
72
+ def inside?( dst, obj )
73
+ o = dst
74
+ while o
75
+ return true if o.equal?( obj )
76
+
77
+ o = o.parent
78
+ end
79
+ return false
80
+ end
81
+
51
82
  #
52
83
  # Lookup the object that we're moving.
53
84
  #
@@ -97,18 +128,26 @@ module Gloo
97
128
  {
98
129
  :name => KEYWORD,
99
130
  :shortcut => KEYWORD_SHORT,
100
- :description => 'Move an object to a new parent.',
131
+ :description => 'Move an object to a new parent. A name is unique ' \
132
+ 'in its container, so the new parent must not already have a ' \
133
+ 'child with the same name.',
101
134
  :syntax => [ 'move {path.to.object} to {new.parent}' ],
102
135
  :parameters => [
103
136
  '{path.to.object} — The object that we want to move.',
104
137
  '{new.parent} — The new location for the object.'
105
138
  ],
106
- :result => 'The object will now be in the new location.',
139
+ :result => 'The object will now be in the new location. Moving it ' \
140
+ 'to the parent it is already in does nothing. A pure action: it ' \
141
+ 'is left alone, unless the move is refused.',
107
142
  :errors => [
108
143
  "#{MISSING_SRC_ERR} The {path.to.object} is not specified.",
109
144
  "Object '{path.to.object}' was not found. — The {path.to.object} cannot be resolved.",
110
145
  "#{MISSING_DST_ERR} The {new.parent} is not specified.",
111
- "Object '{new.parent}' was not found. — The {new.parent} cannot be resolved."
146
+ "Object '{new.parent}' was not found. — The {new.parent} cannot be resolved.",
147
+ "#{format( NAME_TAKEN_ERR, '{path.to.object}', '{new.parent}', '{name}' )} — " \
148
+ 'The new parent already has a child with that name. Nothing moves, and it is false.',
149
+ "#{format( INSIDE_ITSELF_ERR, '{path.to.object}' )} — The new parent is the " \
150
+ 'object itself or something under it. Nothing moves, and it is false.'
112
151
  ],
113
152
  :examples => <<~EXAMPLES.strip
114
153
  can [can] :
@@ -91,8 +91,13 @@ module Gloo
91
91
  ],
92
92
  :parameters => [
93
93
  '{path.to.object} — Name of the object to save.',
94
- '{path} — Where to save it, relative to the project root ' \
95
- '(.gloo appended if omitted). Registers the mapping, so ' \
94
+ '{path} — Where to save it: a full path, one starting ' \
95
+ 'with ~, or a relative name (.gloo appended if omitted). ' \
96
+ 'A relative name goes in the first place a relative load ' \
97
+ 'looks: the app folder in App mode; otherwise next to the ' \
98
+ "saving script's file (the projects folder for a script " \
99
+ 'that did not come from a file), or the working folder for ' \
100
+ 'a command typed at the prompt. Registers the mapping, so ' \
96
101
  'a later bare save includes it.',
97
102
  '( {expression} ) — Instead of a literal path, an expression ' \
98
103
  'in parentheses: it is worked out and its value is used as ' \
@@ -105,7 +110,14 @@ module Gloo
105
110
  'A declaration you never touched comes back byte-for-byte. ' \
106
111
  'An object counts as changed only when its value is set or ' \
107
112
  'it is created, moved, or deleted (its doc comment goes with ' \
108
- 'it); changes to its children do not change it. ' \
113
+ 'it); changes to its children do not change it. A changed ' \
114
+ 'declaration keeps its name, type and spacing; only its ' \
115
+ 'value is written again. A new object goes after the last ' \
116
+ "child of its parent's first declaration, in the file's " \
117
+ 'indentation and type spellings. An object declared on more ' \
118
+ 'than one line has its value on the first: a change rewrites ' \
119
+ 'only that line, and deleting or moving it takes all its ' \
120
+ 'lines. ' \
109
121
  'With a path, the object also stops being declared in its ' \
110
122
  'old file (if it had one) -- both files are rewritten ' \
111
123
  'together.',
@@ -122,7 +134,17 @@ module Gloo
122
134
  'container from nested-container shorthand; extract a ' \
123
135
  'child that does, or a shallower ancestor that does.'
124
136
  ],
125
- :notes => 'An object can also be told to save itself: ' \
137
+ :notes => 'A value that cannot be written exactly (a block ' \
138
+ 'value with no final line break, a line that is just END, ' \
139
+ 'bytes that are not UTF-8) is written as closely as possible, ' \
140
+ "with a warning: Could not save '{path}' exactly: {why}. A " \
141
+ 'changed script body loses the comments and blank lines ' \
142
+ 'inside it. A new top-level object has no file yet: a bare ' \
143
+ 'save leaves it out and warns (Not saved (no file yet): ' \
144
+ '{name}.); save {name} gives it a file of its own, named ' \
145
+ 'after it and placed the same way as a relative path. ' \
146
+ 'See doc verbs, Load & Save. ' \
147
+ 'An object can also be told to save itself: ' \
126
148
  '`tell my_obj to save`. When several files contribute to ' \
127
149
  'one container (the namespace pattern), each file save only ' \
128
150
  "rewrites that file's own declarations. Extraction (save " \
@@ -0,0 +1,34 @@
1
+ #
2
+ # Exit status of a script run.
3
+ # `gloo file.gloo` exits 1 if anything went wrong at any point in the
4
+ # run, not just on its last line. The engine running these tests was
5
+ # started in Test mode, so the check runs gloo again with [system],
6
+ # on a fixture beside this file.
7
+ #
8
+
9
+ tests.app.exit_status [can] :
10
+
11
+ # The interpreter and the fixture, both found from source_dir.
12
+ run_rb [string] : /../../lib/run.rb
13
+ fixture [string] : /fixtures/exit_after_error.gloo
14
+ quiet [string] : --quiet
15
+ # The fixture's error output isn't part of these tests' output.
16
+ discard [string] : > /dev/null 2>&1
17
+ where [string] :
18
+
19
+ run_gloo [system] :
20
+ command [string] :
21
+ get_output [bool] : false
22
+ result [bool] :
23
+
24
+ error_then_success_exits_1 [test] :
25
+ description [string] : A run with an error on an earlier line exits non-zero, though its last line works.
26
+ on_test [script] :
27
+ check ^^ for source_dir
28
+ put it into ^^.where
29
+ # Paths are quoted in case the folder has spaces.
30
+ put "ruby '" ^^.where ^^.run_rb "' " ^^.quiet " '" ^^.where ^^.fixture "' " ^^.discard into ^^.run_gloo.command
31
+ tell ^^.run_gloo to run
32
+ # result is false when the command exits non-zero.
33
+ eval ^^.run_gloo.result
34
+ refute "expected the run to exit non-zero"
@@ -0,0 +1,9 @@
1
+ #
2
+ # Run by exit_status.test.gloo as `gloo` would run it: an error, then
3
+ # a line that works. The run must still exit 1.
4
+ #
5
+ a [can] :
6
+ n [int] : 0
7
+ on_load [script] :
8
+ show no.such.object
9
+ put 1 into a.n