@@ -2,14 +2,19 @@ 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+ PathIndex , PathRow , ProjectionViewport , create_path_indices,
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 ) ]
1115pub struct Document {
1216 rows : Vec < Row > ,
17+ path_indices : Box < [ PathIndex ] > ,
1318 position : usize ,
1419 viewport : Cell < ProjectionViewport > ,
1520}
@@ -31,8 +36,10 @@ impl Document {
3136 }
3237
3338 fn from_rows ( rows : Vec < Row > ) -> Self {
39+ let path_indices = json_path_indices ( & rows) ;
3440 Self {
3541 rows,
42+ path_indices,
3643 position : 0 ,
3744 viewport : Cell :: default ( ) ,
3845 }
@@ -107,6 +114,11 @@ impl Document {
107114 visible_position ( & self . rows , self . position )
108115 }
109116
117+ /// Returns the zero-based document index and jq-style path of the selected row.
118+ pub fn selected_path ( & self ) -> Option < ( usize , String ) > {
119+ path_at_row ( & self . rows , & self . path_indices , self . position )
120+ }
121+
110122 /// Toggles the visibility of a node at the cursor's current position.
111123 pub fn toggle ( & mut self ) {
112124 let index = self . rows . toggle ( self . position ) ;
@@ -130,6 +142,40 @@ impl Document {
130142 row_index_at_visible_position ( & self . rows , self . position , visible_offset)
131143 }
132144
145+ /// Resolves a jq-style path in a zero-based document to its underlying row index.
146+ ///
147+ /// Each top-level JSON value, including each JSON Lines value, increments the document index.
148+ ///
149+ /// Paths use dot notation for identifier keys and bracket notation for array indices and
150+ /// other string keys, for example `.items[0].name` and `["first name"]`.
151+ pub fn row_index_for_path ( & self , document_index : usize , path : & str ) -> Option < usize > {
152+ locate_path ( & self . rows , document_index, path) . map ( |located| located. row_index )
153+ }
154+
155+ /// Moves the cursor to the value at a jq-style path in a zero-based document.
156+ ///
157+ /// Each top-level JSON value, including each JSON Lines value, increments the document index.
158+ ///
159+ /// Folded ancestors are expanded while unrelated folding state is preserved.
160+ pub fn move_to_path ( & mut self , document_index : usize , path : & str ) -> bool {
161+ let Some ( located) = locate_path ( & self . rows , document_index, path) else {
162+ return false ;
163+ } ;
164+ for open_index in located. ancestors {
165+ if matches ! (
166+ self . rows[ open_index] . node,
167+ JsonNode :: Container ( ContainerNode :: Open {
168+ collapsed: true ,
169+ ..
170+ } )
171+ ) {
172+ self . rows . toggle ( open_index) ;
173+ }
174+ }
175+ self . position = located. row_index ;
176+ true
177+ }
178+
133179 pub ( super ) fn row_index_at_viewport_position ( & self , visible_position : usize ) -> Option < usize > {
134180 let viewport = self . viewport . get ( ) ;
135181 viewport
@@ -173,6 +219,120 @@ impl Document {
173219 }
174220}
175221
222+ struct PathFrame {
223+ path : String ,
224+ typ : ContainerType ,
225+ next_index : usize ,
226+ open_index : usize ,
227+ }
228+
229+ struct LocatedRow {
230+ row_index : usize ,
231+ ancestors : Vec < usize > ,
232+ }
233+
234+ fn locate_path ( rows : & [ Row ] , document_index : usize , target : & str ) -> Option < LocatedRow > {
235+ let mut stack: Vec < PathFrame > = Vec :: new ( ) ;
236+ let mut current_document_index = None ;
237+ let mut next_document_index = 0 ;
238+
239+ for ( row_index, row) in rows. iter ( ) . enumerate ( ) {
240+ if matches ! ( row. node, JsonNode :: Container ( ContainerNode :: Close { .. } ) ) {
241+ stack. truncate ( row. depth ) ;
242+ continue ;
243+ }
244+ if row. depth == 0 {
245+ if next_document_index > document_index {
246+ return None ;
247+ }
248+ current_document_index = Some ( next_document_index) ;
249+ next_document_index += 1 ;
250+ stack. clear ( ) ;
251+ }
252+ if current_document_index != Some ( document_index) {
253+ continue ;
254+ }
255+ stack. truncate ( row. depth ) ;
256+
257+ let path = if row. depth == 0 {
258+ "." . to_owned ( )
259+ } else {
260+ let parent = stack. get_mut ( row. depth - 1 ) ?;
261+ match parent. typ {
262+ ContainerType :: Object => append_string_key ( & parent. path , row. key . as_deref ( ) ?) ,
263+ ContainerType :: Array => {
264+ let path = append_bracket ( & parent. path , & parent. next_index . to_string ( ) ) ;
265+ parent. next_index += 1 ;
266+ path
267+ }
268+ }
269+ } ;
270+
271+ if path == target {
272+ return Some ( LocatedRow {
273+ row_index,
274+ ancestors : stack. iter ( ) . map ( |frame| frame. open_index ) . collect ( ) ,
275+ } ) ;
276+ }
277+
278+ if let JsonNode :: Container ( ContainerNode :: Open { typ, .. } ) = & row. node {
279+ stack. push ( PathFrame {
280+ path,
281+ typ : typ. clone ( ) ,
282+ next_index : 0 ,
283+ open_index : row_index,
284+ } ) ;
285+ }
286+ }
287+
288+ None
289+ }
290+
291+ fn json_path_indices ( rows : & [ Row ] ) -> Box < [ PathIndex ] > {
292+ create_path_indices ( rows. iter ( ) . map ( |row| match & row. node {
293+ JsonNode :: Container ( ContainerNode :: Close { .. } ) => PathRow :: Close { depth : row. depth } ,
294+ JsonNode :: Container ( ContainerNode :: Open { typ, .. } ) => PathRow :: Value {
295+ depth : row. depth ,
296+ open_type : Some ( typ. clone ( ) ) ,
297+ } ,
298+ _ => PathRow :: Value {
299+ depth : row. depth ,
300+ open_type : None ,
301+ } ,
302+ } ) )
303+ }
304+
305+ fn path_at_row (
306+ rows : & [ Row ] ,
307+ path_indices : & [ PathIndex ] ,
308+ target_row_index : usize ,
309+ ) -> Option < ( usize , String ) > {
310+ let target_row_index = match & rows. get ( target_row_index) ?. node {
311+ JsonNode :: Container ( ContainerNode :: Close { open_index, .. } ) => * open_index,
312+ _ => target_row_index,
313+ } ;
314+ let mut chain = vec ! [ target_row_index] ;
315+ while rows[ * chain. last ( ) ?] . depth > 0 {
316+ chain. push ( path_indices[ * chain. last ( ) ?] . parent ( ) ?) ;
317+ }
318+
319+ let root_index = * chain. last ( ) ?;
320+ let document_index = path_indices[ root_index] . document_index ( ) ?;
321+ let mut path = "." . to_owned ( ) ;
322+ for & row_index in chain. iter ( ) . rev ( ) . skip ( 1 ) {
323+ let index = path_indices[ row_index] ;
324+ let parent_index = index. parent ( ) ?;
325+ let JsonNode :: Container ( ContainerNode :: Open { typ, .. } ) = & rows[ parent_index] . node else {
326+ return None ;
327+ } ;
328+ path = match typ {
329+ ContainerType :: Object => append_string_key ( & path, rows[ row_index] . key . as_deref ( ) ?) ,
330+ ContainerType :: Array => append_bracket ( & path, & index. array_index ( ) ?. to_string ( ) ) ,
331+ } ;
332+ }
333+ Some ( ( document_index, path) )
334+ }
335+
176336fn visible_position ( rows : & Vec < Row > , target : usize ) -> usize {
177337 let mut position = rows. head ( ) ;
178338 let mut visible = 0 ;
@@ -276,4 +436,78 @@ mod tests {
276436 assert ! ( Document :: from_reader( Cursor :: new( b"[1," ) ) . is_err( ) ) ;
277437 }
278438 }
439+
440+ mod row_index_for_path {
441+ use super :: * ;
442+
443+ #[ test]
444+ fn resolves_nested_values_arrays_and_quoted_keys ( ) {
445+ let document = Document :: from_str (
446+ r#"{"items":[null,{"first name":true}],"true":false,"a\"b\n":0}"# ,
447+ )
448+ . unwrap ( ) ;
449+
450+ assert_eq ! ( document. row_index_for_path( 0 , "." ) , Some ( 0 ) ) ;
451+ assert_eq ! ( document. row_index_for_path( 0 , ".items" ) , Some ( 1 ) ) ;
452+ assert_eq ! ( document. row_index_for_path( 0 , ".items[1]" ) , Some ( 3 ) ) ;
453+ assert_eq ! (
454+ document. row_index_for_path( 0 , r#".items[1]["first name"]"# ) ,
455+ Some ( 4 )
456+ ) ;
457+ assert_eq ! ( document. row_index_for_path( 0 , r#"["true"]"# ) , Some ( 7 ) ) ;
458+ assert_eq ! ( document. row_index_for_path( 0 , r#"["a\"b\n"]"# ) , Some ( 8 ) ) ;
459+ assert_eq ! ( document. row_index_for_path( 0 , ".missing" ) , None ) ;
460+ }
461+
462+ #[ test]
463+ fn distinguishes_json_lines_documents ( ) {
464+ let document = Document :: from_str ( concat ! (
465+ "{\" name\" :\" first\" }\n " ,
466+ "{\" name\" :\" second\" ,\" second_only\" :true}\n " ,
467+ ) )
468+ . unwrap ( ) ;
469+
470+ let first = document. row_index_for_path ( 0 , ".name" ) . unwrap ( ) ;
471+ let second = document. row_index_for_path ( 1 , ".name" ) . unwrap ( ) ;
472+ assert_ne ! ( first, second) ;
473+ assert_eq ! ( document. row_index_for_path( 0 , ".second_only" ) , None ) ;
474+ assert ! ( document. row_index_for_path( 1 , ".second_only" ) . is_some( ) ) ;
475+ assert_eq ! ( document. row_index_for_path( 2 , "." ) , None ) ;
476+ }
477+ }
478+
479+ mod move_to_path {
480+ use super :: * ;
481+
482+ #[ test]
483+ fn expands_ancestors_and_selects_the_target ( ) {
484+ let mut document =
485+ Document :: from_str ( r#"{"items":[{"nested":true}],"other":{"value":1}}"# ) . unwrap ( ) ;
486+ document. toggle_at ( 1 ) ;
487+ document. toggle_at ( 6 ) ;
488+
489+ assert ! ( document. move_to_path( 0 , ".items[0].nested" ) ) ;
490+ assert_eq ! ( document. visible_position( ) , 3 ) ;
491+ assert_eq ! (
492+ document. selected_path( ) ,
493+ Some ( ( 0 , ".items[0].nested" . to_owned( ) ) )
494+ ) ;
495+ assert_eq ! ( document. visible_rows( ) . len( ) , 8 ) ;
496+ assert ! ( !document. move_to_path( 0 , ".missing" ) ) ;
497+ }
498+
499+ #[ test]
500+ fn selects_a_json_lines_document ( ) {
501+ let mut document =
502+ Document :: from_str ( "{\" first_only\" :true}\n {\" second_only\" :true}\n " ) . unwrap ( ) ;
503+
504+ assert ! ( document. move_to_path( 1 , ".second_only" ) ) ;
505+ assert_eq ! (
506+ document. selected_path( ) ,
507+ Some ( ( 1 , ".second_only" . to_owned( ) ) )
508+ ) ;
509+ assert ! ( !document. move_to_path( 0 , ".second_only" ) ) ;
510+ assert ! ( !document. move_to_path( 2 , "." ) ) ;
511+ }
512+ }
279513}
0 commit comments