gloo 6.5.1 → 6.7.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/docs/application.md +2 -0
- data/docs/verbs.md +6 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +12 -0
- data/lib/gloo/app/engine.rb +17 -1
- data/lib/gloo/app/settings.rb +40 -0
- data/lib/gloo/core/tokens.rb +8 -2
- data/lib/gloo/docs/help_shell.rb +46 -1
- data/lib/gloo/objs/basic/string_msgs.rb +20 -0
- data/lib/gloo/objs/basic/word_wrap.rb +58 -0
- data/lib/gloo/persist/file_loader.rb +16 -12
- data/lib/gloo/persist/file_storage.rb +20 -3
- data/lib/gloo/persist/persist_man.rb +31 -1
- data/lib/gloo/persist/source/source_doc.rb +36 -0
- data/lib/gloo/persist/subtree_extractor.rb +184 -0
- data/lib/gloo/persist/trivia_buffer.rb +120 -0
- data/lib/gloo/verbs/if.rb +18 -8
- data/lib/gloo/verbs/list.rb +36 -4
- data/lib/gloo/verbs/save.rb +24 -4
- data/lib/gloo/verbs/unless.rb +13 -4
- data/test.gloo/objs/string.test.gloo +17 -0
- data/test.gloo/objs/text.test.gloo +9 -0
- data/test.gloo/verbs/if.test.gloo +21 -0
- data/test.gloo/verbs/save.test.gloo +86 -0
- data/test.gloo/verbs/unless.test.gloo +21 -0
- metadata +9 -4
- data/lib/gloo/persist/comment_buffer.rb +0 -58
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7bec0a2a7c9eb15015734a7610359cc6b7c18abbc9abbb239131f9fad9927fcf
|
|
4
|
+
data.tar.gz: a9d8810c5581416608f4913d1cd7f1b24e385fb4b1d47b74eb5e1dcd8aaafd47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17694e6f6a9019c0071248eb4c59ab80ffba8c352b7aa5341eeca150d0503e5176eabc0e00a0214d8a99c24e98fa045138dfb7783bb55c1c30198f0704235cec
|
|
7
|
+
data.tar.gz: 6cff2396f6bdc2debed6ab6f03a43e5932a1fa4c5a54a07c40b0e5d18f3686721ca9ae90e05d60ea26fc416c93f66e1acb05de9141e9777afec78406c17181a6
|
data/docs/application.md
CHANGED
|
@@ -88,6 +88,8 @@ gloo:
|
|
|
88
88
|
#
|
|
89
89
|
# Run this script when starting up gloo.
|
|
90
90
|
# (Only if a script file is not specified.)
|
|
91
|
+
# A bare filename (no path) is looked up in the config directory
|
|
92
|
+
# above. Either way, the .gloo extension is assumed if left off.
|
|
91
93
|
#
|
|
92
94
|
start_with:
|
|
93
95
|
|
data/docs/verbs.md
CHANGED
|
@@ -87,16 +87,20 @@ put {expression} into {dst.path}
|
|
|
87
87
|
|
|
88
88
|
If the name can't be resolved to a file, `load` reports `File not found: {name}` — it fires `on_error` and, when the file was named on the `gloo` command line, prints the message to stderr and exits non-zero. A bad or missing file never fails silently.
|
|
89
89
|
|
|
90
|
-
`save` writes loaded objects back to their files. With no argument it saves every open file; with an object it saves the file (or files) that object's tree came from; with `to {path}` it
|
|
90
|
+
`save` writes loaded objects back to their files. With no argument it saves every open file; with an object it saves the file (or files) that object's tree came from; with `to {path}` it **extracts** — moves the object (and its descendants) out of whichever file currently owns them, into a new file, under their full dotted path.
|
|
91
91
|
|
|
92
92
|
```gloo
|
|
93
93
|
> save # every open file
|
|
94
94
|
> save config # just config's file
|
|
95
|
-
> save
|
|
95
|
+
> save app.core.settings to config/settings
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
A save is a **rewrite, not a regeneration**: comments, blank lines, and the original spacing are kept, and only the values you actually changed are re-written. A declaration you never touched comes back byte-for-byte.
|
|
99
99
|
|
|
100
|
+
`save {obj} to {path}` doesn't just write a copy — `obj` stops being declared in its old file, wherever that was. Given `app.core.settings` declared inside `app.gloo`, `save app.core.settings to config/settings` leaves `app.gloo` without a `settings` subtree and writes `config/settings.gloo` with `app.core.settings [can] : ...` at the top level (the `app`/`core` prefix is implied via nested-container shorthand, not repeated as nested declarations) plus everything `settings` owns underneath, comments included. An object with no file of its own yet (brand new, or nested under an object that's never been saved) is simply written fresh — nothing to move.
|
|
101
|
+
|
|
102
|
+
If `obj`'s own subtree already has declarations spread across more than one file (the namespace-merge pattern below, applied *inside* the subtree being extracted, not just above it), extraction refuses rather than guessing which file's slice is authoritative — save each contributing file on its own first.
|
|
103
|
+
|
|
100
104
|
Several files can contribute to one container — declare `app [container] :` in each and add different children. They merge in the heap, and each file's save only rewrites its own declarations. If two files declare the *same* object with different values, the first one loaded wins and `load` logs a warning.
|
|
101
105
|
|
|
102
106
|
An object can also save itself: `tell config to save`.
|
data/lib/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.7.0
|
data/lib/VERSION_NOTES
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
6.7.0 - 2026.09.13
|
|
2
|
+
- Adds alternative secondary keywords for conditional verbs.
|
|
3
|
+
- Adds option to save an object subtree into its own file.
|
|
4
|
+
- Fixes issue with load and save comments and blank lines.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
6.6.0 - 2026.09.12
|
|
8
|
+
- Allow shortcut reference for start file in configuration directory.
|
|
9
|
+
- Adds word wrap message for strings and text.
|
|
10
|
+
- Adds line wrapping for object comments and help documentation.
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
6.5.1 - 2026.09.11
|
|
2
14
|
- Adds doc to the load verb for recent changes.
|
|
3
15
|
|
data/lib/gloo/app/engine.rb
CHANGED
|
@@ -207,7 +207,9 @@ module Gloo
|
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
name = @settings.start_with
|
|
210
|
-
|
|
210
|
+
return if name.blank?
|
|
211
|
+
|
|
212
|
+
@persist_man.load( resolve_start_with( name ) )
|
|
211
213
|
end
|
|
212
214
|
|
|
213
215
|
#
|
|
@@ -382,6 +384,20 @@ module Gloo
|
|
|
382
384
|
|
|
383
385
|
private
|
|
384
386
|
|
|
387
|
+
#
|
|
388
|
+
# Resolve the start_with setting to a loadable name/path.
|
|
389
|
+
# A bare filename (no directory separator) is resolved against
|
|
390
|
+
# the config directory rather than the project path, and the
|
|
391
|
+
# .gloo extension is assumed when not given.
|
|
392
|
+
#
|
|
393
|
+
def resolve_start_with( name )
|
|
394
|
+
ext = @persist_man.file_ext
|
|
395
|
+
name = "#{name}#{ext}" unless name.end_with?( ext )
|
|
396
|
+
return name if name.include?( File::SEPARATOR )
|
|
397
|
+
|
|
398
|
+
return File.join( @settings.config_path, name )
|
|
399
|
+
end
|
|
400
|
+
|
|
385
401
|
#
|
|
386
402
|
# Get the stack trace as a string, truncating the middle if it's long.
|
|
387
403
|
#
|
data/lib/gloo/app/settings.rb
CHANGED
|
@@ -189,13 +189,53 @@ module Gloo
|
|
|
189
189
|
def get_default_settings
|
|
190
190
|
projects = File.join( @user_root, 'projects' )
|
|
191
191
|
str = <<~TEXT
|
|
192
|
+
#
|
|
193
|
+
# Gloo configuration
|
|
194
|
+
#
|
|
192
195
|
gloo:
|
|
196
|
+
|
|
197
|
+
#
|
|
198
|
+
# Root directory for projects.
|
|
199
|
+
# Update this with the directory with your gloo projects.
|
|
200
|
+
#
|
|
193
201
|
project_path: #{projects}
|
|
202
|
+
|
|
203
|
+
#
|
|
204
|
+
# Run this script when starting up gloo.
|
|
205
|
+
# (Only if a script file is not specified.)
|
|
206
|
+
# A bare filename (no path) is looked up in the config directory
|
|
207
|
+
# above. Either way, the .gloo extension is assumed if left off.
|
|
208
|
+
#
|
|
194
209
|
start_with:
|
|
210
|
+
|
|
211
|
+
#
|
|
212
|
+
# Indentation (spaces) when showing an object outline.
|
|
213
|
+
#
|
|
195
214
|
list_indent: 2
|
|
215
|
+
|
|
216
|
+
#
|
|
217
|
+
# Show listing the object tree,
|
|
218
|
+
# how many levels of children will be shown?
|
|
219
|
+
# Children at deeper levels will be hidden.
|
|
220
|
+
#
|
|
196
221
|
list_levels: 3
|
|
222
|
+
|
|
223
|
+
#
|
|
224
|
+
# When listing the object tree, also show each object's doc (the
|
|
225
|
+
# comment declared immediately above it in its source file), if it
|
|
226
|
+
# has one.
|
|
227
|
+
#
|
|
197
228
|
list_docs: false
|
|
229
|
+
|
|
230
|
+
#
|
|
231
|
+
# Show debug statements in the log?
|
|
232
|
+
#
|
|
198
233
|
debug: false
|
|
234
|
+
|
|
235
|
+
#
|
|
236
|
+
# Color theme for console output: dark or light.
|
|
237
|
+
# Match this to your terminal's background.
|
|
238
|
+
#
|
|
199
239
|
theme: #{DEFAULT_THEME}
|
|
200
240
|
TEXT
|
|
201
241
|
return str
|
data/lib/gloo/core/tokens.rb
CHANGED
|
@@ -88,12 +88,18 @@ module Gloo
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
#
|
|
91
|
-
# Get the index of the given token.
|
|
91
|
+
# Get the index of the given token. token may also be an array
|
|
92
|
+
# of candidate keywords -- e.g. [ 'then', 'do' ] -- in which
|
|
93
|
+
# case the index of whichever one occurs earliest in the token
|
|
94
|
+
# stream wins (not just whichever is present). This lets a
|
|
95
|
+
# caller treat several spellings of the same separator as
|
|
96
|
+
# interchangeable while still keying the split on the first one
|
|
97
|
+
# actually used (see If/Unless's 'then'/'do').
|
|
92
98
|
#
|
|
93
99
|
def index_of( token )
|
|
94
100
|
return nil unless @tokens
|
|
95
101
|
|
|
96
|
-
return @tokens.find_index { |o| o.casecmp(
|
|
102
|
+
return Array( token ).filter_map { |t| @tokens.find_index { |o| o.casecmp( t ).zero? } }.min
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
#
|
data/lib/gloo/docs/help_shell.rb
CHANGED
|
@@ -24,6 +24,10 @@ module Gloo
|
|
|
24
24
|
README_GLOB = 'README*'.freeze
|
|
25
25
|
NO_README_YET = 'No README found for library'.freeze
|
|
26
26
|
|
|
27
|
+
# Matches a (possibly indented) markdown bullet line, capturing
|
|
28
|
+
# its leading whitespace and the text after '- '.
|
|
29
|
+
BULLET_RE = /\A(\s*)-\s+(.*)\z/.freeze
|
|
30
|
+
|
|
27
31
|
#
|
|
28
32
|
# Initialize the help shell for the given engine.
|
|
29
33
|
#
|
|
@@ -249,10 +253,51 @@ module Gloo
|
|
|
249
253
|
#
|
|
250
254
|
def page_markdown( md )
|
|
251
255
|
rule = '-' * @engine.platform.cols
|
|
252
|
-
|
|
256
|
+
wrapped = wrap_markdown_for_terminal( md.strip )
|
|
257
|
+
bracketed = "#{rule}\n#{wrapped}\n#{rule}\n"
|
|
253
258
|
@engine.platform.page( Gloo::Docs::MarkdownRenderer.colorize( bracketed, @engine.theme ) )
|
|
254
259
|
end
|
|
255
260
|
|
|
261
|
+
#
|
|
262
|
+
# Word-wrap plain (pre-color) markdown to the terminal width.
|
|
263
|
+
# Headings and fenced code blocks are left untouched (code is
|
|
264
|
+
# never rewrapped); a bullet line's continuation lines are
|
|
265
|
+
# indented to line up under its text, not under the '-' itself
|
|
266
|
+
# -- same convention as List#show_doc's doc-line wrapping.
|
|
267
|
+
#
|
|
268
|
+
def wrap_markdown_for_terminal( md )
|
|
269
|
+
width = Gloo::App::Settings.cols( @engine )
|
|
270
|
+
in_code_fence = false
|
|
271
|
+
lines = md.split( "\n", -1 ).flat_map do |line|
|
|
272
|
+
if line.strip.start_with?( '```' )
|
|
273
|
+
in_code_fence = !in_code_fence
|
|
274
|
+
next [ line ]
|
|
275
|
+
end
|
|
276
|
+
next [ line ] if in_code_fence || line.start_with?( '#' ) || line.strip.empty?
|
|
277
|
+
|
|
278
|
+
wrap_markdown_line( line, width )
|
|
279
|
+
end
|
|
280
|
+
return lines.join( "\n" )
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
#
|
|
284
|
+
# Wrap one prose or bullet markdown line to width. A bullet's
|
|
285
|
+
# continuation lines get a blank-space prefix the same length as
|
|
286
|
+
# its '{indent}- ', so wrapped text lines up under the bullet's
|
|
287
|
+
# own text.
|
|
288
|
+
#
|
|
289
|
+
def wrap_markdown_line( line, width )
|
|
290
|
+
match = BULLET_RE.match( line )
|
|
291
|
+
prefix = match ? "#{match[1]}- " : ''
|
|
292
|
+
text = match ? match[2] : line
|
|
293
|
+
cont_indent = ' ' * prefix.length
|
|
294
|
+
|
|
295
|
+
wrapped = Gloo::Objs::WordWrap.wrap( text, width - prefix.length )
|
|
296
|
+
# "".split( "\n", -1 ) is [], not [ '' ] -- keep a blank line as one piece.
|
|
297
|
+
pieces = wrapped.empty? ? [ '' ] : wrapped.split( "\n", -1 )
|
|
298
|
+
return pieces.each_with_index.map { |piece, i| "#{i.zero? ? prefix : cont_indent}#{piece}" }
|
|
299
|
+
end
|
|
300
|
+
|
|
256
301
|
#
|
|
257
302
|
# Snapshot the verb, object type, doc page, and loaded library
|
|
258
303
|
# names for tab-completion.
|
|
@@ -55,6 +55,7 @@ module Gloo
|
|
|
55
55
|
'splitl ({index}) — Get the substring to the left of index {index} (same as split (0, {index})). A parameter is required. Does not change the value of the string. It will have the substring.',
|
|
56
56
|
'splitr ({index}) — Get the substring from index {index} to the end of the string (same as split ({index}, size)). A parameter is required. Does not change the value of the string. It will have the substring.',
|
|
57
57
|
'split_list ({delim} {dst.path}) — Split the string by {delim} and put the parts into children of the container at {dst.path} (or an alias that points to one), one part per child, in order. Existing children are matched by position and have their values set; extra parts get new (untyped) children, numbered from 1; extra existing children are left alone. Both parameters are required. Does not change the value of the string. It will have the number of parts.',
|
|
58
|
+
'word_wrap ({width}) — Word-wrap the string to {width} columns, breaking on whitespace (never mid-word; an overlong word is left on its own line rather than broken). The {width} parameter is optional; it defaults to the terminal\'s current width. This message changes the value of the string. It will have the wrapped string.',
|
|
58
59
|
'page — Show the value in a pager (less), for viewing long content a screen at a time.'
|
|
59
60
|
]
|
|
60
61
|
end
|
|
@@ -481,6 +482,25 @@ module Gloo
|
|
|
481
482
|
return s
|
|
482
483
|
end
|
|
483
484
|
|
|
485
|
+
#
|
|
486
|
+
# Word-wrap the string to the given column width, breaking on
|
|
487
|
+
# whitespace. Defaults to the terminal's current width when no
|
|
488
|
+
# width parameter is given.
|
|
489
|
+
#
|
|
490
|
+
def msg_word_wrap
|
|
491
|
+
width = Gloo::App::Settings.cols( @engine )
|
|
492
|
+
if @params&.token_count&.positive?
|
|
493
|
+
expr = Gloo::Expr::Expression.new( @engine, @params.tokens )
|
|
494
|
+
data = expr.evaluate
|
|
495
|
+
width = data.to_i
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
s = Gloo::Objs::WordWrap.wrap( value, width )
|
|
499
|
+
set_value s
|
|
500
|
+
@engine.heap.it.set_to s
|
|
501
|
+
return s
|
|
502
|
+
end
|
|
503
|
+
|
|
484
504
|
#
|
|
485
505
|
# Show the value in a pager, for long content.
|
|
486
506
|
#
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Word-wrapping utility.
|
|
5
|
+
# This is a static class.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Gloo
|
|
9
|
+
module Objs
|
|
10
|
+
class WordWrap
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------
|
|
13
|
+
# Wrapping
|
|
14
|
+
# ---------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Wrap the given text to the given column width.
|
|
18
|
+
# Greedy word-wrap: breaks on whitespace, never mid-word. A single
|
|
19
|
+
# word longer than the width is left on its own line rather than
|
|
20
|
+
# being hard-broken (e.g. a URL stays intact, just overflows).
|
|
21
|
+
# Existing line breaks in the text are preserved -- each line is
|
|
22
|
+
# wrapped independently, so blank lines and paragraph breaks survive.
|
|
23
|
+
#
|
|
24
|
+
def self.wrap( text, width )
|
|
25
|
+
return text.to_s if width.to_i <= 0
|
|
26
|
+
|
|
27
|
+
text.to_s.split( "\n", -1 ).map { |line| wrap_line( line, width ) }.join( "\n" )
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------
|
|
31
|
+
# Private
|
|
32
|
+
# ---------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Wrap a single line (no embedded newlines) to the given width.
|
|
36
|
+
#
|
|
37
|
+
def self.wrap_line( line, width )
|
|
38
|
+
words = line.split( ' ' )
|
|
39
|
+
return line if words.empty?
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
current = words.shift
|
|
43
|
+
words.each do |word|
|
|
44
|
+
if "#{current} #{word}".length > width
|
|
45
|
+
lines << current
|
|
46
|
+
current = word
|
|
47
|
+
else
|
|
48
|
+
current = "#{current} #{word}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
lines << current
|
|
52
|
+
return lines.join( "\n" )
|
|
53
|
+
end
|
|
54
|
+
private_class_method :wrap_line
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# raw formatting) -- so a later save can rewrite the file instead of
|
|
8
8
|
# regenerating it from scratch.
|
|
9
9
|
#
|
|
10
|
-
# Work is delegated to
|
|
10
|
+
# Work is delegated to TriviaBuffer (comment/blank-line buffering),
|
|
11
11
|
# ScriptBodyCollector (script bodies), IndentStack (nesting shared by
|
|
12
12
|
# the heap and source trees), ShorthandExpander (nested-container
|
|
13
13
|
# shorthand), and DeclarationLedger (this file's roots + cross-file
|
|
@@ -42,7 +42,7 @@ module Gloo
|
|
|
42
42
|
@pn = pn
|
|
43
43
|
@obj = nil
|
|
44
44
|
@source_doc = Gloo::Persist::Source::SourceDoc.new
|
|
45
|
-
@
|
|
45
|
+
@trivia = Gloo::Persist::TriviaBuffer.new
|
|
46
46
|
@body = Gloo::Persist::ScriptBodyCollector.new
|
|
47
47
|
@shorthand = Gloo::Persist::ShorthandExpander.new( engine )
|
|
48
48
|
@ledger = Gloo::Persist::DeclarationLedger.new( engine, pn )
|
|
@@ -126,7 +126,7 @@ module Gloo
|
|
|
126
126
|
#
|
|
127
127
|
def finish
|
|
128
128
|
@body.finish
|
|
129
|
-
@
|
|
129
|
+
@trivia.flush_into( @indent_stack.node.children )
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# ---------------------------------------------------------------------
|
|
@@ -134,18 +134,22 @@ module Gloo
|
|
|
134
134
|
# ---------------------------------------------------------------------
|
|
135
135
|
|
|
136
136
|
#
|
|
137
|
-
# A comment or blank line, outside of any block/body.
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# and
|
|
137
|
+
# A comment or blank line, outside of any block/body. Both are
|
|
138
|
+
# buffered, not placed immediately: a container's own declaration
|
|
139
|
+
# line doesn't push it as the current node (see IndentStack) until
|
|
140
|
+
# a genuinely deeper line is seen, so a comment/blank sitting
|
|
141
|
+
# between the container and its first child has to wait for that
|
|
142
|
+
# push before it's resolved against the *correct* node -- placing
|
|
143
|
+
# it against whatever's current right now would land it one level
|
|
144
|
+
# too shallow. TriviaBuffer#take_leading_doc/#flush_into do that
|
|
145
|
+
# resolving once the right moment comes (the next declaration, or
|
|
146
|
+
# end of file).
|
|
142
147
|
#
|
|
143
148
|
def handle_trivia_line( line )
|
|
144
149
|
if line.strip.empty?
|
|
145
|
-
@
|
|
146
|
-
@indent_stack.node.children << Source::BlankNode.new( chomped( line ) )
|
|
150
|
+
@trivia.push_blank( chomped( line ) )
|
|
147
151
|
else
|
|
148
|
-
@
|
|
152
|
+
@trivia.push_comment( chomped( line ), tab_count( line ) )
|
|
149
153
|
end
|
|
150
154
|
end
|
|
151
155
|
|
|
@@ -225,7 +229,7 @@ module Gloo
|
|
|
225
229
|
@ledger.root( @last ) if parent == @engine.heap.root
|
|
226
230
|
|
|
227
231
|
node = build_obj_node( leading_ws( line ), name, type, value, style )
|
|
228
|
-
node.leading_doc = @
|
|
232
|
+
node.leading_doc = @trivia.take_leading_doc( line_tabs, @indent_stack.node.children )
|
|
229
233
|
# First non-empty doc wins, same as "first value wins" for a
|
|
230
234
|
# name re-declared across files -- @last is the same object
|
|
231
235
|
# across re-declarations (the factory returns the existing
|
|
@@ -11,14 +11,18 @@ module Gloo
|
|
|
11
11
|
attr_reader :obj, :pn, :roots, :source_doc
|
|
12
12
|
|
|
13
13
|
#
|
|
14
|
-
# Set up a file storage for an object.
|
|
14
|
+
# Set up a file storage for an object. source_doc is optional --
|
|
15
|
+
# pass one already built (eg. a subtree just extracted into this
|
|
16
|
+
# file by save {obj} to {path}) so this file's saves rewrite it
|
|
17
|
+
# in place instead of falling back to plain regeneration; a
|
|
18
|
+
# bare/never-loaded FileStorage leaves it nil.
|
|
15
19
|
#
|
|
16
|
-
def initialize( engine, pn, obj = nil )
|
|
20
|
+
def initialize( engine, pn, obj = nil, source_doc = nil )
|
|
17
21
|
@engine = engine
|
|
18
22
|
@obj = obj
|
|
19
23
|
@pn = pn
|
|
20
24
|
@roots = obj ? [ obj ] : []
|
|
21
|
-
@source_doc =
|
|
25
|
+
@source_doc = source_doc
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
#
|
|
@@ -31,6 +35,19 @@ module Gloo
|
|
|
31
35
|
fs.save
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
#
|
|
39
|
+
# This file no longer owns obj as one of its roots -- eg. it was
|
|
40
|
+
# just extracted into a different file via save {obj} to {path}.
|
|
41
|
+
# Drops it from roots, and re-points the file's primary obj at
|
|
42
|
+
# whatever root remains (nil if none left), so a later
|
|
43
|
+
# single-object reload/save doesn't act on a root that's moved
|
|
44
|
+
# elsewhere.
|
|
45
|
+
#
|
|
46
|
+
def drop_root( obj )
|
|
47
|
+
@roots.delete( obj )
|
|
48
|
+
@obj = @roots.first if @obj&.equal?( obj )
|
|
49
|
+
end
|
|
50
|
+
|
|
34
51
|
#
|
|
35
52
|
# Load the object from the file.
|
|
36
53
|
#
|
|
@@ -63,6 +63,13 @@ module Gloo
|
|
|
63
63
|
# mapping so a future bare save includes it. Refuses to overwrite
|
|
64
64
|
# a file that exists but isn't already mapped to this object.
|
|
65
65
|
#
|
|
66
|
+
# Unlike a bare save, this extracts: obj's own declaration (and
|
|
67
|
+
# its descendants) move out of whichever file currently owns
|
|
68
|
+
# them into the new file -- they don't stay declared in both
|
|
69
|
+
# places. An object with no file of its own yet (brand new, or
|
|
70
|
+
# already mapped as part of some root shared with unrelated
|
|
71
|
+
# objects) is simply written fresh; nothing to remove.
|
|
72
|
+
#
|
|
66
73
|
def save_to( name, path )
|
|
67
74
|
obj = resolve_for_save( name )
|
|
68
75
|
return unless obj
|
|
@@ -72,7 +79,7 @@ module Gloo
|
|
|
72
79
|
return save_mapped( obj, mapped, pn ) if mapped
|
|
73
80
|
return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) if @mech.exist?( pn )
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
extract_to( obj, pn )
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
#
|
|
@@ -333,6 +340,29 @@ module Gloo
|
|
|
333
340
|
@maps << fs
|
|
334
341
|
end
|
|
335
342
|
|
|
343
|
+
#
|
|
344
|
+
# Extract obj's declaration into a brand-new file at pn: pulled
|
|
345
|
+
# out of whichever file currently owns it (that file loses it and
|
|
346
|
+
# gets re-saved), or written fresh if it had no file of its own.
|
|
347
|
+
# Both saves go through the same save_batch so each correctly
|
|
348
|
+
# treats the other's declarations as "owned elsewhere" rather
|
|
349
|
+
# than orphaned/new.
|
|
350
|
+
#
|
|
351
|
+
def extract_to( obj, pn )
|
|
352
|
+
node, source_fs = Gloo::Persist::SubtreeExtractor.new( @engine ).extract( obj, @maps )
|
|
353
|
+
return unless node
|
|
354
|
+
|
|
355
|
+
@engine.event_manager.on_save obj
|
|
356
|
+
source_doc = Gloo::Persist::Source::SourceDoc.new
|
|
357
|
+
source_doc.children << node
|
|
358
|
+
|
|
359
|
+
target_fs = Gloo::Persist::FileStorage.new( @engine, pn, obj, source_doc )
|
|
360
|
+
@maps << target_fs
|
|
361
|
+
# New file first: if writing it fails, the old file (source_fs)
|
|
362
|
+
# hasn't been touched on disk yet, so nothing is lost.
|
|
363
|
+
save_batch( [ target_fs, source_fs ].compact )
|
|
364
|
+
end
|
|
365
|
+
|
|
336
366
|
#
|
|
337
367
|
# Warn (without blocking) if reloading fs would discard changes
|
|
338
368
|
# that haven't been saved -- any object whose value no longer
|
|
@@ -35,6 +35,42 @@ module Gloo
|
|
|
35
35
|
return @children.select { |n| n.is_a?( Source::ObjNode ) }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
#
|
|
39
|
+
# Find and remove the node for the given heap object, searching
|
|
40
|
+
# this document's whole tree (not just the top level) -- used to
|
|
41
|
+
# move a subtree into a different file (see save {obj} to
|
|
42
|
+
# {path}). Returns the removed node, still carrying its own
|
|
43
|
+
# children/leading_doc, or nil if this document has no node for
|
|
44
|
+
# that object (it may be owned by a different file, or never
|
|
45
|
+
# have had a declaration of its own -- eg. a nested-container
|
|
46
|
+
# shorthand's auto-created intermediate).
|
|
47
|
+
#
|
|
48
|
+
def extract( obj )
|
|
49
|
+
return remove_matching( @children, obj )
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Depth-first search of nodes (and their children) for the one
|
|
56
|
+
# linked to obj; removes it from whichever children array it's
|
|
57
|
+
# actually in and returns it.
|
|
58
|
+
#
|
|
59
|
+
def remove_matching( nodes, obj )
|
|
60
|
+
nodes.each do |node|
|
|
61
|
+
next unless node.is_a?( Source::ObjNode )
|
|
62
|
+
|
|
63
|
+
if node.obj&.equal?( obj )
|
|
64
|
+
nodes.delete( node )
|
|
65
|
+
return node
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
found = remove_matching( node.children, obj )
|
|
69
|
+
return found if found
|
|
70
|
+
end
|
|
71
|
+
return nil
|
|
72
|
+
end
|
|
73
|
+
|
|
38
74
|
end
|
|
39
75
|
end
|
|
40
76
|
end
|