Repository: rust-lang/rust
Pull request: rustdoc-search: match path components on words

Since the length of a path is treated as sorting criteria, and every path that contains the query without exactly matching it must be longer, exact matches will always sort first if they exist.

Fixes rust-lang/rust#154733


Changed files:
filename: src/librustdoc/html/static/js/search.js
patch: @@ -2799,6 +2799,8 @@ class DocSearch {
                 result_list.sort((aaa, bbb) => {
                     const aai = aaa.item;
                     const bbi = bbb.item;
+                    const ap = aai.modulePath !== undefined ? aai.modulePath : "";
+                    const bp = bbi.modulePath !== undefined ? bbi.modulePath : "";
                     /** @type {number} */
                     let a;
                     /** @type {number} */
@@ -2829,14 +2831,25 @@ class DocSearch {
                         if (a !== b) {
                             return a - b;
                         }
-                    }
 
-                    // Sort by distance in the path part, if specified
-                    // (less changes required to match means higher rankings)
-                    a = Number(aaa.path_dist);
-                    b = Number(bbb.path_dist);
-                    if (a !== b) {
-                        return a - b;
+                        if (parsedQuery.elems[0] &&
+                            parsedQuery.elems[0].pathWithoutLast.length !== 0
+                        ) {
+                            // Sort by distance in the path part, if specified
+                            // (less changes required to match means higher rankings)
+                            a = Number(aaa.path_dist);
+                            b = Number(bbb.path_dist);
+                            if (a !== b) {
+                                return a - b;
+                            }
+
+                            // sort by path (longer goes later)
+                            a = ap.length + (aai.parent ? aai.parent.name.length + 2 : 0);
+                            b = bp.length + (bbi.parent ? bbi.parent.name.length + 2 : 0);
+                            if (a !== b) {
+                                return a - b;
+                            }
+                        }
                     }
 
                     // (later literal occurrence, if any, goes later)
@@ -2890,8 +2903,8 @@ class DocSearch {
                     }
 
                     // sort by item name (lexicographically larger goes later)
-                    let aw = aai.normalizedName;
-                    let bw = bbi.normalizedName;
+                    const aw = aai.normalizedName;
+                    const bw = bbi.normalizedName;
                     if (aw !== bw) {
                         return (aw > bw ? +1 : -1);
                     }
@@ -2914,12 +2927,8 @@ class DocSearch {
                     }
 
                     // sort by path (lexicographically larger goes later)
-                    const ap = aai.modulePath;
-                    const bp = bbi.modulePath;
-                    aw = ap === undefined ? "" : ap;
-                    bw = bp === undefined ? "" : bp;
-                    if (aw !== bw) {
-                        return (aw > bw ? +1 : -1);
+                    if (ap !== bp) {
+                        return (ap > bp ? +1 : -1);
                     }
 
                     // que sera, sera
@@ -3848,13 +3857,19 @@ class DocSearch {
                 let dist_total = 0;
                 for (let x = 0; x < clength; ++x) {
                     const [p, c] = [path[i + x], contains[x]];
+                    const indexOf = p.indexOf(c);
                     if (parsedQuery.literalSearch && p !== c) {
                         continue pathiter;
-                    } else if (Math.floor((p.length - c.length) / 3) <= maxPathEditDistance &&
-                        p.indexOf(c) !== -1
-                    ) {
+                    } else if (indexOf !== -1) {
                         // discount distance on substring match
-                        dist_total += Math.floor((p.length - c.length) / 3);
+                        // if component is surrounded by underscores or edges,
+                        // count the distance as zero
+                        if (
+                            (indexOf !== 0 && p[indexOf - 1] !== "_") ||
+                            (indexOf + c.length !== p.length && p[indexOf + c.length] !== "_")
+                        ) {
+                            dist_total += Math.floor((p.length - c.length) / 3);
+                        }
                     } else {
                         const dist = editDistance(p, c, maxPathEditDistance);
                         if (dist > maxPathEditDistance) {

filename: tests/rustdoc-js-std/path-maxeditdistance.js
patch: @@ -10,15 +10,24 @@ const EXPECTED = [
         query: 'vec::iter',
         others: [
             // std::net::ToSocketAttrs::iter should not show up here
+            { 'path': 'std::collections::vec_deque', 'name': 'Iter' },
             { 'path': 'std::collections::VecDeque', 'name': 'iter' },
+            { 'path': 'std::collections::vec_deque', 'name': 'IterMut' },
             { 'path': 'std::collections::VecDeque', 'name': 'iter_mut' },
-            { 'path': 'std::vec::Vec', 'name': 'from_iter' },
             { 'path': 'std::vec', 'name': 'IntoIter' },
+            { 'path': 'std::vec::Vec', 'name': 'from_iter' },
             { 'path': 'std::vec::Vec', 'name': 'into_iter' },
-            { 'path': 'std::vec::ExtractIf', 'name': 'into_iter' },
             { 'path': 'std::vec::Drain', 'name': 'into_iter' },
-            { 'path': 'std::vec::IntoIter', 'name': 'into_iter' },
             { 'path': 'std::vec::Splice', 'name': 'into_iter' },
+            { 'path': 'std::vec::IntoIter', 'name': 'into_iter' },
+            { 'path': 'std::vec::ExtractIf', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque', 'name': 'IntoIter' },
+            { 'path': 'std::collections::vec_deque::Iter', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque::Drain', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque::Splice', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque::IterMut', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque::IntoIter', 'name': 'into_iter' },
+            { 'path': 'std::collections::vec_deque::ExtractIf', 'name': 'into_iter' },
             { 'path': 'std::collections::VecDeque', 'name': 'from_iter' },
             { 'path': 'std::collections::VecDeque', 'name': 'into_iter' },
         ],

filename: tests/rustdoc-js/path-substring.js
patch: @@ -0,0 +1,75 @@
+// exact-check
+// ignore-tidy-linelength
+const EXPECTED = [
+    // should match (substring)
+    {
+        'query': 'struct:now::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    {
+        'query': 'struct:is::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    {
+        'query': 'struct:is_the::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    {
+        'query': 'struct:the::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    {
+        'query': 'struct:their::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    // should not match
+    {
+        'query': 'struct:ood::Country',
+        'others': [],
+    },
+    {
+        'query': 'struct:goo::Country',
+        'others': [],
+    },
+    {
+        'query': 'struct:he::Country',
+        'others': [],
+    },
+    {
+        'query': 'struct:heir::Country',
+        'others': [],
+    },
+    {
+        'query': 'struct:hei::Country',
+        'others': [],
+    },
+    {
+        'query': 'struct:no::Country',
+        'others': [],
+    },
+    // should match (edit distance)
+    {
+        'query': 'struct:nowisthetimeforallgoodmentocometotheaidoftheir::Country',
+        'others': [
+            { 'path': 'x::nowisthetimeforallgoodmentocometotheaidoftheir', 'name': 'Country' },
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+        ],
+    },
+    {
+        'query': 'struct:now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their::Country',
+        'others': [
+            { 'path': 'x::now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their', 'name': 'Country' },
+            { 'path': 'x::nowisthetimeforallgoodmentocometotheaidoftheir', 'name': 'Country' },
+        ],
+    },
+];

filename: tests/rustdoc-js/path-substring.rs
patch: @@ -0,0 +1,7 @@
+#![crate_name = "x"]
+pub mod now_is_the_time_for_all_good_men_to_come_to_the_aid_of_their {
+    pub struct Country;
+}
+pub mod nowisthetimeforallgoodmentocometotheaidoftheir {
+    pub struct Country;
+}