@@ -2,9 +2,13 @@ use std::{cell::Cell, io::Read};
22
33use super :: {
44 deserializer,
5- jsonz:: { self , Row , RowOperation } ,
5+ jsonz:: { self , ContainerNode , ContainerType , JsonNode , Row , RowOperation } ,
6+ } ;
7+ use crate :: structured:: {
8+ ProjectionViewport ,
9+ path:: { append_bracket, append_string_key} ,
10+ projection_viewport,
611} ;
7- use crate :: structured:: { ProjectionViewport , projection_viewport} ;
812
913/// Represents a navigable JSON document, allowing for efficient row navigation and folding.
1014#[ derive( Clone ) ]
@@ -130,6 +134,36 @@ impl Document {
130134 row_index_at_visible_position ( & self . rows , self . position , visible_offset)
131135 }
132136
137+ /// Resolves a jq-style path to its underlying document row index.
138+ ///
139+ /// Paths use dot notation for identifier keys and bracket notation for array indices and
140+ /// other string keys, for example `.items[0].name` and `["first name"]`.
141+ pub fn row_index_for_path ( & self , path : & str ) -> Option < usize > {
142+ locate_path ( & self . rows , path) . map ( |located| located. row_index )
143+ }
144+
145+ /// Moves the cursor to the value at a jq-style path.
146+ ///
147+ /// Folded ancestors are expanded while unrelated folding state is preserved.
148+ pub fn move_to_path ( & mut self , path : & str ) -> bool {
149+ let Some ( located) = locate_path ( & self . rows , path) else {
150+ return false ;
151+ } ;
152+ for open_index in located. ancestors {
153+ if matches ! (
154+ self . rows[ open_index] . node,
155+ JsonNode :: Container ( ContainerNode :: Open {
156+ collapsed: true ,
157+ ..
158+ } )
159+ ) {
160+ self . rows . toggle ( open_index) ;
161+ }
162+ }
163+ self . position = located. row_index ;
164+ true
165+ }
166+
133167 pub ( super ) fn row_index_at_viewport_position ( & self , visible_position : usize ) -> Option < usize > {
134168 let viewport = self . viewport . get ( ) ;
135169 viewport
@@ -173,6 +207,62 @@ impl Document {
173207 }
174208}
175209
210+ struct PathFrame {
211+ path : String ,
212+ typ : ContainerType ,
213+ next_index : usize ,
214+ open_index : usize ,
215+ }
216+
217+ struct LocatedRow {
218+ row_index : usize ,
219+ ancestors : Vec < usize > ,
220+ }
221+
222+ fn locate_path ( rows : & [ Row ] , target : & str ) -> Option < LocatedRow > {
223+ let mut stack: Vec < PathFrame > = Vec :: new ( ) ;
224+
225+ for ( row_index, row) in rows. iter ( ) . enumerate ( ) {
226+ if matches ! ( row. node, JsonNode :: Container ( ContainerNode :: Close { .. } ) ) {
227+ stack. truncate ( row. depth ) ;
228+ continue ;
229+ }
230+ stack. truncate ( row. depth ) ;
231+
232+ let path = if row. depth == 0 {
233+ "." . to_owned ( )
234+ } else {
235+ let parent = stack. get_mut ( row. depth - 1 ) ?;
236+ match parent. typ {
237+ ContainerType :: Object => append_string_key ( & parent. path , row. key . as_deref ( ) ?) ,
238+ ContainerType :: Array => {
239+ let path = append_bracket ( & parent. path , & parent. next_index . to_string ( ) ) ;
240+ parent. next_index += 1 ;
241+ path
242+ }
243+ }
244+ } ;
245+
246+ if path == target {
247+ return Some ( LocatedRow {
248+ row_index,
249+ ancestors : stack. iter ( ) . map ( |frame| frame. open_index ) . collect ( ) ,
250+ } ) ;
251+ }
252+
253+ if let JsonNode :: Container ( ContainerNode :: Open { typ, .. } ) = & row. node {
254+ stack. push ( PathFrame {
255+ path,
256+ typ : typ. clone ( ) ,
257+ next_index : 0 ,
258+ open_index : row_index,
259+ } ) ;
260+ }
261+ }
262+
263+ None
264+ }
265+
176266fn visible_position ( rows : & Vec < Row > , target : usize ) -> usize {
177267 let mut position = rows. head ( ) ;
178268 let mut visible = 0 ;
@@ -276,4 +366,44 @@ mod tests {
276366 assert ! ( Document :: from_reader( Cursor :: new( b"[1," ) ) . is_err( ) ) ;
277367 }
278368 }
369+
370+ mod row_index_for_path {
371+ use super :: * ;
372+
373+ #[ test]
374+ fn resolves_nested_values_arrays_and_quoted_keys ( ) {
375+ let document = Document :: from_str (
376+ r#"{"items":[null,{"first name":true}],"true":false,"a\"b\n":0}"# ,
377+ )
378+ . unwrap ( ) ;
379+
380+ assert_eq ! ( document. row_index_for_path( "." ) , Some ( 0 ) ) ;
381+ assert_eq ! ( document. row_index_for_path( ".items" ) , Some ( 1 ) ) ;
382+ assert_eq ! ( document. row_index_for_path( ".items[1]" ) , Some ( 3 ) ) ;
383+ assert_eq ! (
384+ document. row_index_for_path( r#".items[1]["first name"]"# ) ,
385+ Some ( 4 )
386+ ) ;
387+ assert_eq ! ( document. row_index_for_path( r#"["true"]"# ) , Some ( 7 ) ) ;
388+ assert_eq ! ( document. row_index_for_path( r#"["a\"b\n"]"# ) , Some ( 8 ) ) ;
389+ assert_eq ! ( document. row_index_for_path( ".missing" ) , None ) ;
390+ }
391+ }
392+
393+ mod move_to_path {
394+ use super :: * ;
395+
396+ #[ test]
397+ fn expands_ancestors_and_selects_the_target ( ) {
398+ let mut document =
399+ Document :: from_str ( r#"{"items":[{"nested":true}],"other":{"value":1}}"# ) . unwrap ( ) ;
400+ document. toggle_at ( 1 ) ;
401+ document. toggle_at ( 6 ) ;
402+
403+ assert ! ( document. move_to_path( ".items[0].nested" ) ) ;
404+ assert_eq ! ( document. visible_position( ) , 3 ) ;
405+ assert_eq ! ( document. visible_rows( ) . len( ) , 8 ) ;
406+ assert ! ( !document. move_to_path( ".missing" ) ) ;
407+ }
408+ }
279409}
0 commit comments