Action

Block level filter (MGCL/search)

Posted by @jsamlarose, Last update over 3 years ago

UPDATES

over 3 years ago

Now supports inline hashtags. Also a few cosmetic adjustments.

show all updates...

over 3 years ago

Now supports inline hashtags. Also a few cosmetic adjustments.

over 3 years ago

Update to fix bug when starting search from another action (you can send text to this from another action or from a shortcut; prefix incoming text as “fromWikiLink:”) or from selected text. The first action step (preferences) now determines defaults for the way the action behaves, but can be overwritten by the first prompt if that setting is enabled.

over 3 years ago

Updates!

over 3 years ago

Now with added settings!

  • The first action step allows you to control your preferences via the first action step (i.e. set it and forget it) or on the fly via the form. - If you manage your preferences in the action step you can set a few more options in the search scope (inbox, all, flagged, archive, trashed).
  • If you choose to manage your preferences on the fly, the form displays two switches to control whether the full text of a draft is displayed, and whether you want to search in the “inbox” or “all” scope.

over 3 years ago

Minor fix

over 3 years ago

  • added another option: you can now set the filter options to search within your inbox, archive, flagged drafts or even trash
  • tidied up some sections of the code to make it easier to parse for people making their own adjustments (lots more could be done to tidy this up further!)
  • trapped an error that could be triggered by running the filter on a line containing an empty wiki-link

over 3 years ago

Added an easy option to set whether or not a draft’s full text is displayed in instances where the term you’re searching for is found in that draft’s title. The option is determined by a variable in the first action step.

over 3 years ago

Better support for wiki-links/backlinks/(whatever you call them)! The initial prompt now offers options for search queries based on the current draft’s title— nods to @yvonnezed. Also updated the explanation of the kind of search this action does— nods to @cfritze.

over 3 years ago

Updated regex for detecting wiki-links in current line, and handling of [[s: … ]] syntax definition (as well as my own [[@: … ]] syntax).

over 3 years ago

If the search term is in the title, a search result will now show the content of the whole draft in the search item’s context field (description). Felt like a waste for the context to simply repeat the title, otherwise. If the search item is in the title AND some lines within the content, you’ll see those specific lines in the results as well. I might walk this back at some point in the future, but I’m feeling like “more is more” in this circumstance. We’ll see. I’ve also set the list to render HTML and markdown, which makes links a bit tidier, but might break the list in some circumstances. Again, might walk that back, but it’s working well for me for now…

over 3 years ago

Update to facilitate running this action from another action…

over 3 years ago

Minor update: initial prompt (if no text selected) gets focus when invoked.

over 3 years ago

Better handling of relative dates

over 3 years ago

Adjusted description.

Depends on Matt Gemmell’s MGCheckListPrompt action. This won’t work if you don’t have that installed (with apologies to Matt for my own less-than-elegant hackery around his brilliant work…;)

This action queries drafts for a search term…
- if there’s any text selected, that text will be used as a search.
- if no text is selected, you’ll be offered a prompt to enter a search term
- if no text is selected, and the cursor is in a line with a wiki-linked phrase, that wiki-linked phrase is offered as an option for the search— if there’s more than one, each will be offered as a separate option

Once your search term is set, hits are loaded into an interface (the Matt Gemmell Checklist Prompt), displaying the entirety of each line that the search term is found in for context.

Selecting any option from the list should highlight the search term in the draft.

NOTES:
- The action currently has one setting for easy adjustment: if you’d prefer that the list shows the full text of a draft if the term you’re searching for is found in the title of that draft, set the variable in the first action step to be true. Otherwise, leave it false.
- The search that this action performs is an “exact phrase” search, meaning that if you search for a phrase consisting of more than one word, you’ll only see results containing exactly that phrase, rather than results that contain each of the words in that phrase. To borrow an example from Drafts’ guide on search and filtering: “red parachute"will find a draft with the sentence “She used a red parachute when skydiving”, but not the draft “She used a red and blue parachute…” (…also, none of the other advanced query options detailed in that guide are enabled here!)

Steps

  • script

    // PREFERENCES 
    
    // META: Do you want to set preferences here or on the form itself? Doing it here keeps the form tidy and allows for a wider range of options in search scope. Doing it on the form allows you to change things on the fly. Quotation marks required. 
    
    // Options: "here", "form". 
    
    var preferences = "form"
    
    // Don't edit this next line... ;)
    
    // if (preferences == "here") {
    
    // If you've selected "form", any adjustments you make below will be ignored, except when starting a search from a wikilink or from selected text...
    
    // CONTEXT DISPLAY: How would you prefer the list to behave when the term you're searching for is found in the title of a draft? If it would be useful to see the whole draft in this instance, set this variable to true. Otherwise, if you have lots of long drafts, you may wish to keep this set as false, in which case the first non-empty line of the draft is returned. No quotation marks required.
    
    // Options: true, false
    
    	var fullDraftsAsContext = false
    
    // SCOPE OF SEARCH: Where do you want your search to be run? Quotation marks required!
    
    // Options: "inbox", "archive", "flagged", "trash","all".
    
    	var searchScope = "inbox"
    
    // SORT ORDER: How do you want your search results sorted? Quotation marks required!
    
    // Options: "modified", "created"
    
    	var modOrCreated = "modified"
    
    	// (Don't change this...)
    	
    	var mocProp = modOrCreated + "At"
    
    // }
  • includeAction

    name
    MGCheckListPrompt Library
  • script

    // INITIAL PROMPT TO DEFINE FILTER
    
    // the way the action is invoked determines whether this prompt is displayed...
    
    if (editor.getSelectedText().length > 1) {
    	var searchStr = editor.getSelectedText()
    } else if (draft.content.startsWith("fromWikiLink:")) {
    	var searchStr = draft.content.replace("fromWikiLink:","")
    } else { 
    
    	// no selection, and it wasn't invoked from a wikilink, so let's build the prompt...
    	
    	var searchStr = ""
    	lSel = editor.getSelectedLineRange()
    	sel = editor.getTextInRange(lSel[0],lSel[1])
    
    	var p = Prompt.create();
    	p.title = "Filter";
    
    	p.addTextField("searchTerm", "Search for", "", {
      		"wantsFocus": true
    	});
    	
    	p.addLabel("label","ALTERNATIVELY, FILTER BY:", {
    		"textSize": "caption"
    	});
    	
        // options for backlinks...
    
    	var linkRefs = ["(Linked references for this draft)","(Unlinked references for this draft)"];
    
    	// is there a wiki-link in the current line? Let's check, and if so, add to the list of potential filters...
    	var buildArr = []; 
    	
    	if (/\B#(\d*[A-Za-z_]+\w*)\b(?!;)/.test(sel)){ 
    		buildArr = [...new Set(buildArr.concat(sel.match(/\B#(\d*[A-Za-z_]+\w*)\b(?!;)/g)))];
    		}
    
    	if  (/\[\[[\#|\w|\s|@|\.|\?|»|\:|\(|\)|\/|\||\-|\+"]+\]\]/.test(sel)) {
    		buildArr = [...new Set(buildArr.concat(sel.match(/\[\[[\#|\w|\s|@|\.|\?|»|\:|\(|\)|\/|\||\-|\+"]+\]\]/g)))]} 
    	
    	var opts = buildArr.concat(linkRefs)
    
    	p.addSelect("linkSel", "", opts, [], false);
    
    	// show search settings if enabled in first action step...
    
    	if (preferences == "form") {
    	
    		p.addSwitch("modOrCreated", "Sort by modified? (off = created)", false);
    		p.addSwitch("context", "Show full draft if search term is in title?", false);
    		p.addSwitch("searchScope", "Search all? (off = search inbox only)", false);
    
    	}
    	
    	p.addButton("run");
    
    	var didSelect = p.show();
    
    	// set variables and settings in response to chosen/selected terms...
    		
    	if (p.buttonPressed == "run") {
    		if (preferences == "form") {
    
    			var fullDraftsAsContext = p.fieldValues["context"]
    			var searchScope = p.fieldValues["searchScope"] ? "all" : "inbox"
    			var modOrCreated = p.fieldValues["modOrCreated"] ? "modified" : "created"
    			var mocProp = modOrCreated + "At"
    		}
    
    		if (p.fieldValues["searchTerm"].length > 0){
    			searchStr = p.fieldValues["searchTerm"]
    		} else if (p.fieldValues["linkSel"].toString().startsWith("[[s:")||p.fieldValues["linkSel"].toString().startsWith("[[@:")){
    			// a syntax defined search should just be a search term... 
    			searchStr = p.fieldValues["linkSel"].toString().replace("[[s:","").replace("[[@:","").replace("]]","")
    		} else if (p.fieldValues["linkSel"].toString().includes("(Linked references for this draft)")){
    			searchStr = p.fieldValues["linkSel"].toString()
    			var queries = [
      				"[["+ draft.displayTitle +"]]",
      				"[[# "+ draft.displayTitle +"]]",
    			]
    		} else if (p.fieldValues["linkSel"].toString().includes("(Unlinked references for this draft)")){
    			var searchStr = draft.displayTitle;
    		} else if (/\B#(\d*[A-Za-z_]+\w*)\b(?!;)/.test(p.fieldValues["linkSel"].toString())){
    			var searchStr = p.fieldValues["linkSel"].toString();
    		} else if (p.fieldValues["linkSel"].length > 0){
    			// searchStr = p.fieldValues["linkSel"].toString().replace("[[s:","[[")
    			searchStr = p.fieldValues["linkSel"].toString()
    			searchStr = searchStr.split('[[').pop().split(']]')[0].replace("# ","")
    			// searchStr = "\"[[" + searchStr + "\" OR \"[[# " + searchStr + "\""
    			var queries = [
      				"[["+ searchStr +"]]",
      				"[[# "+ searchStr +"]]",
    			];
    		} else {
    			context.cancel()
    		}
    
    	}
    
    } 
    		
    searchStr == "" ? context.cancel() : searchStr
    
  • script

    // FUNCTIONS
    
    // escape characters for regex 
    
    function escapeRegex(string) {
        return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    }
    
    // relative time formatting: https://stackoverflow.com/a/37802747
    
    function timeDifference(previous) {
    	var prefix = "(" + modOrCreated + " "
    	var suffix = ")"
    	var output = ""
    	var current = new Date()
        var msPerMinute = 60 * 1000;
        var msPerHour = msPerMinute * 60;
        var msPerDay = msPerHour * 24;
        var msPerMonth = msPerDay * 30;
        var msPerYear = msPerDay * 365;
    
        var elapsed = current - previous;
    
        if (elapsed < msPerMinute) {
        	     if (Math.round(elapsed/1000)==1) {
        	     output = 'a second ago';
        	     } else {
             output = Math.round(elapsed/1000) + ' seconds ago';   
             }
        }
    
        else if (elapsed < msPerHour) {
             if (Math.round(elapsed/msPerMinute)==1) {
        	     output = 'a minute ago';
        	     } else {
             output = Math.round(elapsed/msPerMinute) + ' minutes ago';   
             }   
        }
    
        else if (elapsed < msPerDay ) {
             if (Math.round(elapsed/msPerHour)==1) {
        	     output = 'an hour ago';
        	     } else {
             output = Math.round(elapsed/msPerHour) + ' hours ago';   
             }
        }
    
        else if (elapsed < msPerMonth) {
            if (Math.round(elapsed/msPerDay)==1) {
        	     output = 'yesterday';
        	     } else {
            output = '~' + Math.round(elapsed/msPerDay) + ' days ago';   
             }
        }
    
        else if (elapsed < msPerYear) {
            if (Math.round(elapsed/msPerMonth)==1) {
        	     output = 'last month';
        	     } else {
             output = '~' + Math.round(elapsed/msPerMonth) + ' months ago';   
             }
        }
    
        else {
            if (Math.round(elapsed/msPerYear)==1) {
        	     output = 'last year';
        	     } else {
             output = '~' + Math.round(elapsed/msPerYear) + ' years ago'; 
             }
        }
    	return prefix + output + suffix
    }
    
    // https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
    
    /* function timeSince(date) {
    
      var seconds = Math.floor((new Date() - date) / 1000);
    
      var interval = seconds / 31536000;
    
      if (interval > 1) {
        return Math.floor(interval) + " years";
      }
      interval = seconds / 2592000;
      if (interval > 1) {
        return Math.floor(interval) + " months";
      }
      interval = seconds / 86400;
      if (interval > 1) {
        return Math.floor(interval) + " days";
      }
      interval = seconds / 3600;
      if (interval > 1) {
        return Math.floor(interval) + " hours";
      }
      interval = seconds / 60;
      if (interval > 1) {
        return Math.floor(interval) + " minutes";
      }
      return Math.floor(seconds) + " seconds";
    }
    
    */
    
    
  • script

    // DISPLAY RESULTS AND NAVIGATE TO LOCATIONS WITHIN SELECTED DRAFTS
    
    var i = 0;
    var backlinks = []
    
    if (queries) { 
    
    	// this, if we're searching for a linked reference
    
    	let queriesDD = [...new Set(queries)];
    	var searchStr = "\"" + queriesDD.join("\" OR \"") + "\""
    	let drafts = Draft.query(searchStr, searchScope,[],[],modOrCreated,true)
    
    	while(drafts[i]){
    	var q=0
    	while(queries[q]){
        		hits = drafts[i].lines.filter(element => element.toString().toUpperCase().includes(queries[q].toString().toUpperCase()) )
        		var n=0
        		while(hits[n]){
        			// descr = (typeof hits[n] == 'undefined') ? drafts[i].title : hits[n]
        			let descr = hits[n] ? hits[n] : drafts[i].title
        			let separator = drafts[i].tags.join() == "" ? "" :  " ... "
    
    			backlinks.push({
    				title: drafts[i].title,
    				description: descr,
    				// info: drafts[i].tags.join()+ timeDifference(drafts[i].createdAt),
    				info: drafts[i].tags.join(", ") + separator + timeDifference(drafts[i][mocProp]),
    				uuid: drafts[i].uuid,
    				metadata: drafts[i].content,
    				permalink: drafts[i].permalink
            		});
        			n++;
        		}
    		q++;
    	}
    	i++;
    	}
    
    	const
    	keys = ['title', 'description'],
        		filtered = backlinks.filter(
            		(s => o => 
                		(k => !s.has(k) && s.add(k))
                		(keys.map(k => o[k]).join('|'))
            		)
            		(new Set)
        		);
    } else {
    
    	// all other searches...
    
    	let drafts = Draft.query(searchStr, searchScope,[],[],modOrCreated,true)
    	while(drafts[i]){
    
        		hits = drafts[i].lines.filter(element => element.toString().toUpperCase().includes(searchStr.toString().toUpperCase()) )
        		var n=0
        		while(hits[n]){
        		if (fullDraftsAsContext == true) {
        			descr = drafts[i].title == hits[n] ? drafts[i].processTemplate("[[body]]") : hits[n]
        			} else {
        			// descr = drafts[i].title == hits[n] ? "" : hits[n]
        			descr = drafts[i].title == hits[n] ? draft.lines.filter((a) => a)[1] + "\n↳ _**first line of draft; search term in title**_" : hits[n]
    			}
    			let separator = drafts[i].tags.join() == "" ? "" :  " ... "
    			
    			backlinks.push({
    						title: drafts[i].title,
    						// description: hits[n],
    						description: descr.replace(/\t/g,""),
    						// info: drafts[i].tags.join()+ timeDifference(drafts[i][mocProp]),
    						info: drafts[i].tags.join(", ") + separator + timeDifference(drafts[i][mocProp]),
    						metadata: hits[n],
    						uuid: drafts[i].uuid
            			});
        		n++;
        		}
    	i++;
    	}
    
    }
    
    // Create an MGCheckListPrompt.
    var prompt = new MGCheckListPrompt();
    prompt.message = "Query: " + searchStr;
    prompt.addItems(backlinks)
    prompt.allowsTypeToFilter = true
    prompt.singleSelectionMode = true
    prompt.selectsImmediately = true
    prompt.processMarkdown = true
    prompt.includedContent = `
    <style>
    .item-description {
    	font-size: 90%;
    	display: inline;
    }
    </style>
    `
    
    // Show the prompt.
    var selectedItems = prompt.show();
    
    // Report the result.
    if (prompt.didShow) {
    	if (selectedItems != null) {
    		source = Draft.find(backlinks[selectedItems[0]].uuid);
    		editor.load(source)
    		fullText = editor.getText()
    		anchor = backlinks[selectedItems[0]].metadata
    		pos = fullText.indexOf(anchor)
    		// find position of selected line
    		// pos = fullText.search("/"+ escapeRegex(anchor) +"/i")
    		if (queries) {searchStr = queries[0].toString().substr(2).slice(0, -2)}
    		pos = fullText.toUpperCase().indexOf(searchStr.toString().toUpperCase(), pos) // this to select the search term
    		editor.setSelectedRange(pos,searchStr.toString().length) // anchor.length or searchStr.length, to select search term
    		editor.activate();
    	} else {
    		
    	}
    } else {
    	app.displayErrorMessage("Looks like your query returned no results.");
    	context.cancel()
    }
    

Options

  • After Success Nothing
    Notification None
    Log Level None
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.