@@ -7,6 +7,7 @@ import { decode } from "../util.js";
77import resource from "./DocumentationViewer.blp" ;
88
99import Shortcuts from "./Shortcuts.js" ;
10+ import { hasMatch , score } from "./fzy.js" ;
1011
1112import {
1213 action_extensions ,
@@ -24,13 +25,29 @@ const DocumentationPage = GObject.registerClass(
2425 GObject . ParamFlags . READWRITE ,
2526 "" ,
2627 ) ,
28+ tag : GObject . ParamSpec . string (
29+ "tag" ,
30+ "tag" ,
31+ "Tag of symbol" ,
32+ GObject . ParamFlags . READWRITE ,
33+ "" ,
34+ ) ,
2735 search_name : GObject . ParamSpec . string (
2836 "search_name" ,
2937 "search_name" ,
3038 "Name used to search the item in sidebar" ,
3139 GObject . ParamFlags . READWRITE ,
3240 "" ,
3341 ) ,
42+ score : GObject . ParamSpec . double (
43+ "score" ,
44+ "score" ,
45+ "Score assigned when searching an item in the sidebar" ,
46+ GObject . ParamFlags . READWRITE ,
47+ Number . MIN_SAFE_INTEGER ,
48+ Number . MAX_SAFE_INTEGER ,
49+ Number . MIN_SAFE_INTEGER ,
50+ ) ,
3451 uri : GObject . ParamSpec . string (
3552 "uri" ,
3653 "uri" ,
@@ -62,9 +79,11 @@ export default function DocumentationViewer({ application }) {
6279 const button_back = builder . get_object ( "button_back" ) ;
6380 const button_forward = builder . get_object ( "button_forward" ) ;
6481 const stack = builder . get_object ( "stack" ) ;
65- const browse_list_view = builder . get_object ( "browse_list_view " ) ;
82+ const status_page = builder . get_object ( "status_page " ) ;
6683 const browse_page = builder . get_object ( "browse_page" ) ;
84+ const browse_list_view = builder . get_object ( "browse_list_view" ) ;
6785 const search_page = builder . get_object ( "search_page" ) ;
86+ const search_list_view = builder . get_object ( "search_list_view" ) ;
6887 const search_entry = builder . get_object ( "search_entry" ) ;
6988 const button_shortcuts = builder . get_object ( "button_shortcuts" ) ;
7089
@@ -170,35 +189,21 @@ export default function DocumentationViewer({ application }) {
170189 }
171190 } ) ;
172191
173- const expr = new Gtk . ClosureExpression (
174- GObject . TYPE_STRING ,
175- ( item ) => item . search_name ,
176- null ,
177- ) ;
178- const filter_model = builder . get_object ( "filter_model" ) ;
179- const filter = filter_model . filter ;
180- filter . expression = expr ;
181-
182192 function onSearchChanged ( ) {
183193 if ( search_entry . text ) {
184194 stack . visible_child = search_page ;
185- filter . search = search_entry . text ;
195+ const selection_model = search_list_view . model ;
196+ selection_model . unselect_item ( selection_model . selected ) ;
197+ selection_model . model . model . filter = createFilter ( search_entry . text ) ;
198+ if ( ! selection_model . n_items ) stack . visible_child = status_page ;
199+ search_list_view . scroll_to ( 0 , Gtk . ListScrollFlags . NONE , null ) ;
186200 } else {
187201 stack . visible_child = browse_page ;
188202 }
189203 }
190204
191205 search_entry . connect ( "search-changed" , onSearchChanged ) ;
192206
193- const search_model = builder . get_object ( "search_model" ) ;
194- const sorter = builder . get_object ( "search_sorter" ) ;
195- sorter . expression = expr ;
196- search_model . connect ( "selection-changed" , ( ) => {
197- const uri = search_model . selected_item . uri ;
198- const sidebar_path = URI_TO_SIDEBAR_PATH [ uri ] ;
199- selectSidebarItem ( browse_list_view , sidebar_path ) ;
200- } ) ;
201-
202207 let promise_load ;
203208 async function load ( ) {
204209 if ( ! promise_load ) {
@@ -211,7 +216,10 @@ export default function DocumentationViewer({ application }) {
211216 scanLibraries ( root_model , Gio . File . new_for_path ( "/app/share/doc" ) ) ,
212217 ] ) . then ( ( ) => {
213218 const search_model = flattenModel ( root_model ) ;
214- filter_model . model = search_model ;
219+ search_list_view . model = createSearchSelectionModel (
220+ search_model ,
221+ browse_list_view ,
222+ ) ;
215223 } ) ;
216224 }
217225 return promise_load ;
@@ -305,6 +313,7 @@ async function loadLibrary(model, directory) {
305313 const namespace = `${ index . meta . ns } -${ index . meta . version } ` ;
306314 const page = new DocumentationPage ( {
307315 name : namespace ,
316+ tag : "namespace" ,
308317 search_name : namespace ,
309318 uri : html_file . get_uri ( ) ,
310319 children : getChildren ( index , directory ) ,
@@ -388,6 +397,78 @@ function createBrowseSelectionModel(root_model, webview) {
388397 return selection_model ;
389398}
390399
400+ function createSearchSelectionModel ( model , browse_list_view ) {
401+ const filter_model = Gtk . FilterListModel . new ( model , null ) ;
402+ const sorter = Gtk . CustomSorter . new ( ( item1 , item2 ) => {
403+ return Math . sign ( item2 . score - item1 . score ) ;
404+ } ) ;
405+ const sort_model = new Gtk . SortListModel ( {
406+ model : filter_model ,
407+ sorter : sorter ,
408+ } ) ;
409+ const selection_model = Gtk . SingleSelection . new ( sort_model ) ;
410+ selection_model . autoselect = false ;
411+ selection_model . can_unselect = true ;
412+ selection_model . connect ( "selection-changed" , ( ) => {
413+ if ( ! selection_model . selected_item ) return ;
414+ const uri = selection_model . selected_item . uri ;
415+ const sidebar_path = URI_TO_SIDEBAR_PATH [ uri ] ;
416+ selectSidebarItem ( browse_list_view , sidebar_path ) ;
417+ } ) ;
418+ return selection_model ;
419+ }
420+
421+ const QUERY_TYPES = [
422+ "additional" ,
423+ "alias" ,
424+ "bitfield" ,
425+ "callback" ,
426+ "class" ,
427+ "constant" ,
428+ "constructor" ,
429+ "enum" ,
430+ "error" ,
431+ "function" ,
432+ "interface" ,
433+ "namespace" ,
434+ "macro" ,
435+ "method" ,
436+ "property" ,
437+ "signal" ,
438+ "struct" ,
439+ "union" ,
440+ "vfunc" ,
441+ ] ;
442+
443+ const QUERY_PATTERN = new RegExp (
444+ "^(" + QUERY_TYPES . join ( "|" ) + ")\\s*:\\s*" ,
445+ "i" ,
446+ ) ;
447+
448+ function createFilter ( search_term ) {
449+ const matches = search_term . match ( QUERY_PATTERN ) ;
450+ let tag = null ;
451+
452+ if ( matches ) {
453+ tag = matches [ 1 ] . toLowerCase ( ) ;
454+ search_term = search_term . substring ( matches [ 0 ] . length ) ;
455+ }
456+
457+ const needle = search_term . replace ( / \s + / g, "" ) ;
458+ const isCaseSensitive = needle . toLowerCase ( ) !== needle ;
459+ const actualNeedle = isCaseSensitive ? needle : needle . toLowerCase ( ) ;
460+
461+ return Gtk . CustomFilter . new ( ( item ) => {
462+ const haystack = isCaseSensitive
463+ ? item . search_name
464+ : item . search_name . toLowerCase ( ) ;
465+ const match = hasMatch ( actualNeedle , haystack ) ;
466+ const shouldKeep = match && ( ! tag || item . tag === tag ) ;
467+ if ( shouldKeep ) item . score = score ( actualNeedle , haystack ) ;
468+ return shouldKeep ;
469+ } ) ;
470+ }
471+
391472const SECTION_TYPES = {
392473 class : [ "Classes" , "#classes" ] ,
393474 content : [ "Addition Documentation" , "#extra" ] ,
@@ -438,6 +519,7 @@ function getChildren(index, dir) {
438519 location . insert_sorted (
439520 new DocumentationPage ( {
440521 name : symbol . name ,
522+ tag : getTagForDocument ( symbol ) ,
441523 search_name : getSearchNameForDocument ( symbol , index . meta ) ,
442524 uri : `${ dir . get_uri ( ) } /${ getLinkForDocument ( symbol ) } ` ,
443525 } ) ,
@@ -566,3 +648,25 @@ function getLinkForDocument(doc) {
566648 return `vfunc.${ doc . type_name } .${ doc . name } .html` ;
567649 }
568650}
651+
652+ function getTagForDocument ( doc ) {
653+ switch ( doc . type ) {
654+ case "method" :
655+ case "class_method" :
656+ return "method" ;
657+ case "content" :
658+ return "additional" ;
659+ case "ctor" :
660+ return "constructor" ;
661+ case "domain" :
662+ return "error" ;
663+ case "function_macro" :
664+ return "macro" ;
665+ case "record" :
666+ return "struct" ;
667+ case "type_func" :
668+ return "function" ;
669+ default :
670+ return doc . type ;
671+ }
672+ }
0 commit comments