diff --git a/pd/nw/bookmark.svg b/pd/nw/bookmark.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ddc28c52f08b5dfeaf68b9e85eb319a140c37700
--- /dev/null
+++ b/pd/nw/bookmark.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      </style>
+  </defs>
+ <path style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="m4 2v12l4-1.594 4 1.594v-12h-7zm1 1h6v9.594l-3-1.188-3 1.188v-2.594z"
+     class="ColorScheme-Text"
+     />
+</svg>
diff --git a/pd/nw/bookmark2.svg b/pd/nw/bookmark2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4bfd3facacfb8941d06192b991d75c9514c7515f
--- /dev/null
+++ b/pd/nw/bookmark2.svg
@@ -0,0 +1,22 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      .ColorScheme-NegativeText {
+        color:#da4453;
+      }
+      </style>
+  </defs>
+ <g transform="translate(-421.71-531.79)">
+ <path 
+     style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="m425.71 533.79v12l4-1.594v-1l-3 1.188v-9.594h6v5h1v-6h-7z"
+     class="ColorScheme-Text"/>
+<path style="fill:currentColor;fill-opacity:1;stroke:none" 
+     class="ColorScheme-NegativeText"
+     d="m431.42 540.82l-.707.707 1.793 1.793-1.793 1.793.707.707 1.793-1.793 1.793 1.793.707-.707-1.793-1.793 1.793-1.793-.707-.707-1.793 1.793z" 
+     />
+</g>
+</svg>
diff --git a/pd/nw/dialog_search.html b/pd/nw/dialog_search.html
index 6727ce498cfce3e7b2d64eaf6b3a77e6833bcd35..4c72e35fcb9c2212abe8425bbd12ef575a3fd18d 100644
--- a/pd/nw/dialog_search.html
+++ b/pd/nw/dialog_search.html
@@ -135,6 +135,172 @@ var toc = [
     },
 ];
 
+// ag: Some functions to add bookmarks to the toc, and save them in the user's
+// personal config file.
+
+function toc_add_bookmarks()
+{
+    toc[toc.length] = {
+        title: "Bookmarks",
+    };
+}
+
+function toc_bookmarks()
+{
+    for (var i = 0, len = toc.length; i < len; i++) {
+	if (!toc[i].id && !toc[i].description &&
+	    toc[i].title == "Bookmarks") {
+	    return i;
+	}
+    }
+    return -1;
+}
+
+function expand_tilde(filepath) {
+    if (filepath[0] === '~') {
+        var home = pdgui.check_os("win32") ? process.env.HOMEPATH :
+	    process.env.HOME;
+        return path.join(home, filepath.slice(1));
+    }
+    return filepath;
+}
+
+const toc_config = expand_tilde(
+    pdgui.check_os("win32")
+	? "~/AppData/Roaming/Purr-Data/bookmarks.json"
+	: "~/.purr-data/bookmarks.json"
+);
+
+function toc_save()
+{
+    var i = toc_bookmarks();
+    if (i >= 0) {
+	// the actual bookmarks start at index i+1, i is the section title
+	var data = JSON.stringify(toc.slice(i+1), null, 2);
+	fs.writeFileSync(toc_config, data);
+    } else {
+	// no bookmarks, get rid of the bookmarks file if present
+	try {
+	    fs.unlinkSync(toc_config);
+	} catch (err) {
+	    // ignore
+	}
+    }
+}
+
+// this should be executed just once, when the browser is first shown
+function toc_load()
+{
+    function toc_valid(doc) {
+	// validate the bookmark entries
+	if (doc.id && doc.title) {
+	    try {
+		fs.accessSync(check_dir(doc.id), fs.F_OK);
+		return true;
+	    } catch (e) {
+		return false;
+	    }
+	} else {
+	    return false;
+	}
+    };
+    var bookmarks;
+    try {
+	bookmarks = fs.readFileSync(toc_config);
+	try {
+	    bookmarks = JSON.parse(bookmarks);
+	} catch (err) {
+	    // might be a syntax error, if the user edited the file manually,
+	    // so lets provide some (hopefully useful) diagnostic in the
+	    // console
+	    pdgui.post("error reading bookmarks: "+toc_config, "error");
+	    pdgui.post(err);
+	    return;
+	}
+    } catch (err) {
+	// no bookmarks, just bail out
+	return;
+    }
+    try {
+	// this might still fail if the JSON we read is some (syntactically
+	// correct) random garbage, in this case give some diagnostic below 
+	bookmarks = bookmarks.filter(toc_valid);
+	if (bookmarks && bookmarks.length > 0) {
+	    toc_add_bookmarks();
+	    toc = toc.concat(bookmarks);
+	    // this message would be shown each time the browser is opened,
+	    // which would create a lot of noise; commented, but we leave it
+	    // in here since it might be useful for debugging purposes
+	    //pdgui.post("loaded bookmarks: "+toc_config);
+	}
+    } catch (err) {
+	pdgui.post("error reading bookmarks: "+toc_config, "error");
+	pdgui.post("format error (expected an array, got " +
+		   (typeof bookmarks === "object" ? "an " :
+		    typeof bookmarks === "undefined" ? "" : "a ") +
+		   typeof bookmarks + ")");
+    }
+}
+
+function toc_add_bookmark(id, title, descr)
+{
+    if (toc_bookmarks() < 0) {
+	toc_add_bookmarks();
+    }
+    toc[toc.length] = descr ? {
+	id: id,
+	title: title,
+	description: descr
+    } : {
+	id: id,
+	title: title
+    };
+    pdgui.post("add bookmark: "+title+" ("+id+")");
+    toc_save();
+}
+
+function toc_delete_bookmark(id, title)
+{
+    var i = toc_bookmarks();
+    if (i >= 0) {
+	var l = toc.length;
+	while (i < l &&
+	       (toc[i].id !== id || toc[i].title !== title)) {
+	    i++;
+	}
+	if (i < l) {
+	    toc.splice(i, 1);
+	    pdgui.post("delete bookmark: "+title+" ("+id+")");
+	} else {
+	    // not found, we're done
+	    return;
+	}
+    } else {
+	// no bookmarks, we're done
+	return;
+    }
+    if (toc_bookmarks() == toc.length-1) {
+	// empty bookmark section, remove the section title
+	toc.pop();
+    }
+    toc_save();
+}
+
+function toc_is_bookmarked(id)
+{
+    var i = toc_bookmarks();
+    if (i >= 0) {
+	var l = toc.length;
+	while (i < l && toc[i].id !== id) {
+	    i++;
+	}
+	return i<l;
+    } else {
+	// no bookmarks
+	return false;
+    }
+}
+
 // Stop-gap translator
 function translate_form() {
     var elements = document.querySelectorAll("[data-i18n]"),
@@ -157,6 +323,45 @@ function click_toc(dir) {
 
 var current_dir;
 
+function canonical_path(dir)
+{
+    // normalize
+    dir = path.normalize(dir);
+    // get rid of Windows' '\', '/' works just as well and is more portable
+    return pdgui.defunkify_windows_path(dir);
+}
+
+function check_dir(dirname)
+{
+    var absname = dirname;
+    if (!path.isAbsolute(dirname)) {
+	// A relative path is taken relative to libdir, so that, e.g., doc/*
+	// and extra/* can be used to access documentation in the doc and
+	// extra hierarchies.
+	absname = path.join(pdgui.get_lib_dir(), dirname);
+    }
+    try {
+	if (fs.lstatSync(absname).isDirectory())
+	    return canonical_path(absname);
+    } catch (err) {
+    }
+    return null;
+}
+
+function toc_bookmark_update(dir)
+{
+    var bookmark = document.getElementById("bookmark_indicator");
+    var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir));
+    dir = canonical_path(dir);
+    var id = dir.length <= rel.length ? dir : rel;
+    bookmark.src = toc_is_bookmarked(id) ? "bookmark2.svg" : "bookmark.svg";
+}
+
+function toc_bookmark_status(enabled) {
+    document.getElementById("bookmark_indicator").style.
+	setProperty("opacity", enabled?"1.0":"0.5");
+}
+
 function display_toc() {
     var results_elem = document.getElementById("results"),
         div,
@@ -165,13 +370,16 @@ function display_toc() {
         text_node;
     // reset current_dir to doc
     current_dir = path.join(pdgui.get_lib_dir(), "doc");
+    toc_bookmark_update(current_dir);
+    toc_bookmark_status(false);
     toc.forEach(function(doc, i, a) {
         div = document.createElement("div");
 	if (doc.id) {
 	    try {
-		fs.accessSync(path.join(pdgui.get_lib_dir(), doc.id), fs.F_OK);
+		fs.accessSync(check_dir(doc.id), fs.F_OK);
 		a = document.createElement("a");
-		a.href = "javascript: click_toc('" + doc.id + "');";
+		// We need to properly stringify click_toc's argument here.
+		a.href = "javascript: click_toc(" + JSON.stringify(doc.id) + ");";
 		a.textContent = doc.title;
 		// set title to path for tooltip
 		a.title = doc.id;
@@ -202,6 +410,7 @@ function finish_build(idx) {
     index = idx;
     document.getElementById("search_text").disabled = false;
     clear_results();
+    toc_load();
     display_toc();
 }
 
@@ -230,8 +439,11 @@ function display_directory_callback(err, files) {
 
 function display_directory(dir) {
     current_dir = dir;
+    var is_doc = current_dir === path.join(pdgui.get_lib_dir(), "doc");
     clear_results();
     fs.readdir(dir, display_directory_callback);
+    toc_bookmark_update(dir);
+    toc_bookmark_status(!is_doc);
 }
 
 function file_browser_click() {
@@ -242,12 +454,23 @@ function file_browser_click() {
     document.getElementById("file_browser").click();
 }
 
+function bookmark_indicator_click() {
+    toggle_bookmark(current_dir);
+}
+
 function file_browser_callback(elem) {
     var doc = elem.value;
     if (doc !== "") {
-        pdgui.doc_open(pdgui.defunkify_windows_path(path.dirname(doc)),
-            pdgui.defunkify_windows_path(path.basename(doc)));
-        display_directory(pdgui.defunkify_windows_path(path.dirname(doc)));
+        var defunkify = pdgui.defunkify_windows_path;
+        var dir = path.dirname(doc);
+        pdgui.doc_open(defunkify(dir), defunkify(path.basename(doc)));
+        display_directory(defunkify(dir));
+        // update the search field accordingly; use a relative path if that
+        // makes sense, and canonicalize
+        var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir));
+        dir = canonical_path(dir);
+        dir = dir.length <= rel.length ? dir : rel;
+        document.getElementById("search_text").value = dir;
     }
 }
 
@@ -389,6 +612,12 @@ function window_close_shortcut(evt) {
     return (evt.keyCode === 87 && evt[modifier]) // <ctrl-w>
 }
 
+function bookmark_shortcut(evt) {
+    var osx = process.platform === "darwin",
+    modifier = osx ? "metaKey" : "ctrlKey";
+    return (evt.keyCode === 68 && evt[modifier]) // <ctrl-d> adds, <ctrl-shift-d> deletes a bookmark
+}
+
 function toggle_find_bar() {
     // this is copied from index.js m.edit.find...
     var find_div = document.getElementById("console_find"),
@@ -412,6 +641,65 @@ function toggle_find_bar() {
     }
 }
 
+// Adds (or deletes, if del is true) a bookmark to the toc.
+function do_bookmark(dirname, del)
+{
+    if (current_dir === path.join(pdgui.get_lib_dir(), "doc")) {
+	/* We don't want to bookmark the doc directory. There's nothing
+	   interesting to see there anyway, and, since the toc also lives
+	   there, just bailing out at this point we prevent an interesting
+	   race condition which arises if we try to update the toc while we're
+	   displaying it. */
+	return;
+    }
+    /* id (dirname) for the bookmark. We take this relative to the libdir if
+       the relative designation is shorter than the absolute path, which gives
+       prettier ids for documents in the doc and extra hierarchies and
+       siblings. Note that these ids only ever appear in the toc, never in
+       documents in the search database. */
+    var relname = canonical_path(path.relative(pdgui.get_lib_dir(), dirname));
+    dirname = canonical_path(dirname);
+    var id = dirname.length <= relname.length ? dirname : relname;
+    // Default name for the bookmark.
+    var name = path.basename(dirname);
+    // Let's check whether the directory contains a meta file from which we
+    // may get name and description of the external.
+    var meta = path.join(dirname, name+"-meta.pd");
+    var meta_name, meta_descr;
+    try {
+        var data = fs.readFileSync(meta, 'utf8').replace("\n", " ");
+	meta_name = data.match
+	(/#X text \-?[0-9]+ \-?[0-9]+ NAME ([\s\S]*?);/i);
+	meta_descr = data.match
+	(/#X text \-?[0-9]+ \-?[0-9]+ DESCRIPTION ([\s\S]*?);/i);
+	meta_name = meta_name && meta_name.length > 1 ?
+	    meta_name[1].trim() : null;
+	meta_descr = meta_descr && meta_descr.length > 1 ?
+	    meta_descr[1].trim() : null;
+	// Remove the Pd escapes for commas
+	meta_descr = meta_descr ?
+	    meta_descr.replace(" \\,", ",") : null;
+    } catch (err) {
+        // ignore
+    }
+    name = meta_name ? meta_name : name;
+    if (del)
+	toc_delete_bookmark(id, name);
+    else
+	toc_add_bookmark(id, name, meta_descr);
+    toc_bookmark_update(dirname);
+}
+
+// Toggle bookmark for the given directory. This is invoked by clicking on
+// the bookmark indicator to the right of the search field. 
+function toggle_bookmark(dir)
+{
+    var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir));
+    dir = canonical_path(dir);
+    var id = dir.length <= rel.length ? dir : rel;
+    do_bookmark(dir, toc_is_bookmarked(id));
+}
+
 function add_events() {
     // closing the Window
     nw.Window.get().on("close", function() {
@@ -450,16 +738,32 @@ function add_events() {
             }
     });
 
+    document.getElementById("bookmark_indicator").addEventListener("click",
+        function(evt) {
+            if (evt.currentTarget === document.activeElement) {
+                bookmark_indicator_click();
+            }
+    });
+
     // Keydown in the document
     document.body.addEventListener("keydown", function(evt) {
         var input_elem = document.getElementById("search_text"),
-            button_elem = document.getElementById("file_browser_button");
+            button_elem = document.getElementById("file_browser_button"),
+            button_elem2 = document.getElementById("bookmark_indicator");
         if (find_bar_shortcut(evt)) {
             toggle_find_bar();
-        } else if (evt.target === button_elem &&
+        } else if ((evt.target === button_elem ||
+		    evt.target === button_elem2) &&
                    evt.keyCode === 10 || evt.keyCode === 13) {
         } else if (evt.target !== input_elem) {
             input_elem.focus();
+        } else if (bookmark_shortcut(evt)) {
+	    // We assume here that current_dir is set and points to the
+	    // directory to be bookmarked.
+	    if (current_dir) {
+                evt.stopPropagation();
+		do_bookmark(current_dir, evt.shiftKey);
+	    }
         } else if (window_close_shortcut(evt)) {
                 evt.stopPropagation();
                 pdgui.remove_dialogwin("search");
@@ -494,6 +798,7 @@ function register_window_id(id, attrs) {
 
 function display_no_results() {
     document.getElementById("results").textContent = l("search.no_results");
+    toc_bookmark_update(current_dir);
 }
 
 function display_doc(doc) {
@@ -516,6 +821,7 @@ function display_doc(doc) {
 	div.appendChild(text_node);
     }
     results_elem.appendChild(div);
+    toc_bookmark_update(current_dir);
 }
 
 function doc_search() {
@@ -532,16 +838,16 @@ function doc_search() {
         display_toc();
         return;
     }
-    // if the search term is doc/* or extra/* then short circuit
+    // if the search term is a directory then short circuit
     // the search and just list the docs in that directory
-    if ((search_text.slice(0, 4) === "doc/" ||
-         search_text.slice(0, 6) === "extra/") &&
-        search_text.indexOf(" ") === -1) {
-        display_directory(path.join(pdgui.get_lib_dir(), search_text));
+    var dirname = check_dir(search_text);
+    if (dirname) {
+        display_directory(dirname);
         return;
     }
     clear_results();
     text_elem.blur();
+    toc_bookmark_status(false);
     results = index.search(search_text);
     for (i = 0; i < results.length; i++) {
         doc = index.documentStore.getDoc(results[i].ref);
@@ -573,10 +879,14 @@ function doc_search() {
              name="search_text"
              id="search_text"
              data-i18n="[title]search.search">
-      <input type="image"
+      <input type="image" style="vertical-align:middle;"
              src="folder.svg"
              id="file_browser_button"
              data-i18n="[title]search.browse">
+      <input type="image" style="vertical-align:middle;"
+             src="bookmark.svg"
+             id="bookmark_indicator"
+             data-i18n="[title]search.bookmark">
    </form>
     <div id="results">
     </div>
diff --git a/pd/nw/folder.svg b/pd/nw/folder.svg
index 6d9ea6e9f4feaf79f5bbb42ae2b8377a37e6f25c..337ef4b71e45ea78255edfd64ce02901e4c4df6d 100644
--- a/pd/nw/folder.svg
+++ b/pd/nw/folder.svg
@@ -1 +1,13 @@
-<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 32 32"><defs><clipPath><path d="m69.63 12.145h-.052c-22.727-.292-46.47 4.077-46.709 4.122-2.424.451-4.946 2.974-5.397 5.397-.044.237-4.414 23.983-4.122 46.71-.292 22.777 4.078 46.523 4.122 46.761.451 2.423 2.974 4.945 5.398 5.398.237.044 23.982 4.413 46.709 4.121 22.779.292 46.524-4.077 46.761-4.121 2.423-.452 4.946-2.976 5.398-5.399.044-.236 4.413-23.981 4.121-46.709.292-22.777-4.077-46.523-4.121-46.761-.453-2.423-2.976-4.946-5.398-5.397-.238-.045-23.984-4.414-46.71-4.122"/></clipPath><linearGradient gradientUnits="userSpaceOnUse" y2="352.98" x2="-601.15" y1="663.95" x1="-591.02" id="2"><stop stop-color="#a0a0a0"/><stop offset="1" stop-color="#aaa"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" y2="354.29" x2="-704.05" y1="647.77" x1="-701.19" id="1"><stop stop-color="#acabab"/><stop offset="1" stop-color="#d4d4d4"/></linearGradient><linearGradient id="0" x1="59.12" y1="-19.888" x2="59.15" y2="-37.783" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.17478 0 0 4.16765-1069.7 447.73)"><stop stop-color="#a0a0a0"/><stop offset="1" stop-color="#bdbdbd"/></linearGradient></defs><g transform="matrix(.07089 0 0 .07017 23.295-40.67)" fill="#60aae5"><path transform="matrix(.7872 0 0 .79524 415.34 430.11)" d="m-884.1 294.78c-4.626 0-8.349 3.718-8.349 8.335v161.41l468.19 1v-121.2c0-4.618-3.724-8.335-8.35-8.335h-272.65c-8.51.751-9.607-.377-13.812-5.981-5.964-7.968-14.969-21.443-20.84-29.21-4.712-6.805-5.477-6.02-13.292-6.02z" fill="url(#0)" color="#000"/><rect transform="matrix(.7872 0 0 .79524 415.34 430.11)" y="356.85" x="-890.28" height="295.13" width="463.85" fill="url(#1)" stroke="url(#1)" stroke-width="2.378" rx="9.63"/><rect width="463.85" height="295.13" x="-890.28" y="356.85" transform="matrix(.7872 0 0 .79524 415.34 430.11)" fill="none" stroke="url(#2)" stroke-linejoin="round" stroke-linecap="round" stroke-width="5.376" rx="9.63"/></g></svg>
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      </style>
+  </defs>
+ <path style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="M 2 2 L 2 3 L 2 6 L 2 7 L 2 13 L 2 14 L 14 14 L 14 13 L 14 6 L 14 5 L 14 4 L 9.0078125 4 L 7.0078125 2 L 7 2.0078125 L 7 2 L 3 2 L 2 2 z M 3 3 L 6.5917969 3 L 7.59375 4 L 7 4 L 7 4.0078125 L 6.9921875 4 L 4.9921875 6 L 3 6 L 3 3 z M 3 7 L 13 7 L 13 13 L 3 13 L 3 7 z "
+     class="ColorScheme-Text"
+     />
+</svg>
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index 798fc5cb9852ab30480f068447d0c7e3eb5868ab..6a43d138c9d81288f3996f6ae8460fa0df4de399 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -499,6 +499,7 @@
   },
   "search": {
     "browse": "Durchsuche die Dokumentation",
+    "bookmark": "Lesezeichen hinzufügen oder entfernen",
     "search": "Suche",
     "building_index": "Erstelle Index...",
     "no_results": "Keine Resultate gefunden.",
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index 2a24c5494589e9f1628308087fe25ff875d08527..b54d90c993b803af05c4e412ae7f566a661a9a12 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -499,6 +499,7 @@
   },
   "search": {
     "browse": "browse the documentation",
+    "bookmark": "add or remove a bookmark",
     "search": "search",
     "building_index": "Building index...",
     "no_results": "No results found.",
diff --git a/pd/nw/locales/fr/translation.json b/pd/nw/locales/fr/translation.json
index 6e476f33beca113d02f129c80f7236862c11a8fc..59099097fa6e7b616d36514515c99bd0efc622d5 100644
--- a/pd/nw/locales/fr/translation.json
+++ b/pd/nw/locales/fr/translation.json
@@ -499,6 +499,7 @@
   },
   "search": {
     "browse": "Parcourir la documentation",
+    "bookmark": "Ajouter ou enlever un favori",
     "search": "Chercher",
     "building_index": "Construction de l'index...",
     "no_results": "Aucun résultat trouvé !",