Action

Autocomplete Cross-Link

Posted by @mattgemmell, Last update almost 4 years ago

Autocomplete [[cross-links]]
by Matt Gemmell - @mattgemmell - https://mattgemmell.com/
Type “[[” then at least 2 characters (by default) before invoking this action.
Requires Drafts 20.1 or later.

Steps

  • script

    // Autocomplete [[cross-links]]
    // by Matt Gemmell - @mattgemmell - https://mattgemmell.com/
    // 
    // Type "[[" then at least 2 characters (by default) before invoking this action.
    // Requires Drafts 20.1 or later.
    
    
    // ===========================
    
    
    var chooseBestMatch = false; // If false, user is asked to choose between multiple matching titles.
    
    
    var minimumQueryLength = 2; // Must type at least this number of characters after the "[[" opening delimiter or we won't search.
    
    
    var excludeCurrentDraft = true; // If true, the draft currently being edited will be excluded as a candidate for autocompletion of cross-links. This avoids the situation where autocompleting a cross-link within the first line (i.e. the title) of a draft will always include the draft itself in the list of suggestions.
    
    
    // ===========================
    
    
    function minRange(theRange) {
    	// Returns index of start of theRange.
    	return theRange[0];
    }
    
    function maxRange(theRange) {
    	// Returns index immediately AFTER end of theRange.
    	return theRange[0] + theRange[1];
    }
    
    function editorTextInRange(editor, range) {
    	// Returns editor's text in the given range.
    	return editor.getTextInRange(range[0], range[1]);
    }
    
    function getTitle(theDraft) {
    	return theDraft.displayTitle;
    }
    
    function getUUID(theDraft) {
    	return theDraft.uuid;
    }
    
    
    // Grab entire line(s) of text which contain the selection.
    var selectedLineRange = editor.getSelectedLineRange();
    var selectedLines = editorTextInRange(editor, selectedLineRange);
    
    // If selection spans more than one line, abort.
    var newLineIndex = selectedLines.indexOf("\n");
    if (newLineIndex !== -1 && newLineIndex!== selectedLines.length - 1) { // i.e. Just a trailing newline.
    	app.displayInfoMessage("Doesn't work with multi-line selections.");
    	
    } else {
    	var selRange = editor.getSelectedRange();
    	selRange[0] -= selectedLineRange[0]; // Normalise to be line-relative.
    	var partialTitle = null;
    	var partialTitleRange = null;
    	var openingDelimiter = "[[";
    	
    	if (selRange[1] > 0) {
    		// There's a selection. Assume it's the partial title to be autocompleted.
    		partialTitle = editorTextInRange(selRange);
    		partialTitleRange = selRange;
    		if (partialTitle.indexOf(openingDelimiter) == 0) {
    			// Trim delimiter.
    			partialTitle = partialTitle.slice(openingDelimiter.length);
    			partialTitleRange[0] += openingDelimiter.length;
    			partialTitleRange[1] -= openingDelimiter.length;
    		}
    		
    	} else {
    		// No selection. Try to find opening link-delimiters "[[" to left of insertion point.
    	
    		// Limit the amount of text we're searching for the closest match to a reasonable length.
    		var searchRadius = 250; // Num chars around selection/insertion to search.
    		
    		var searchRange = [0, selectedLines.length]; // Entire line.
    		if (minRange(selRange) > searchRadius) {
    			// Move beginning of searchRange to within searchRadius of selRange's start.
    			searchRange[0] = minRange(selRange) - searchRadius;
    			searchRange[1] -= searchRange[0];
    		}
    		// Move end of searchRange to beginning of selRange (i.e. just behind the insertion point).
    		var delta = maxRange(searchRange) - minRange(selRange);
    		searchRange[1] -= delta;
    		var searchText = selectedLines.substring(minRange(searchRange), maxRange(searchRange));
    		
    		// Find rightmost occurrence of openingDelimiter within searchText.
    		var foundIndex = searchText.lastIndexOf(openingDelimiter);
    		if (foundIndex != -1) {
    			partialTitle = searchText.slice(foundIndex + openingDelimiter.length);
    			partialTitleRange = selRange;
    			partialTitleRange[0] -= partialTitle.length;
    			partialTitleRange[1] = partialTitle.length;
    		}
    	}
    	
    	if (partialTitle != null) {
    		// Check that partialTitle is long enough to be worth searching for.
    		if (partialTitle.length < minimumQueryLength) {
    			app.displayInfoMessage("Please type at least " + minimumQueryLength + " characters for autocompletion.");
    			
    		} else {
    			// Search for matching drafts.
    			partialTitle = partialTitle.trim();
    			var chosenDraft = null;
    			var foundDrafts = Draft.queryByTitle(partialTitle);
    			
    			// Eliminate current draft from results.
    			if (excludeCurrentDraft && foundDrafts.length > 0) {
    				var foundUUIDs = foundDrafts.map(getUUID);
    				var index = foundUUIDs.indexOf(draft.uuid);
    				if (index > -1) {
    					foundDrafts.splice(index, 1); // modifies in place
    				}
    			}
    			
    			if (foundDrafts.length > 0) {
    				// We found at least one match.
    				if (foundDrafts.length == 1 || chooseBestMatch) {
    					// Use the first/only match.
    					chosenDraft = foundDrafts[0];
    					
    				} else {
    					// Choose one of the matching drafts.
    					var draftTitles = foundDrafts.map(getTitle);
    					
    					var prompt = Prompt.create();
    					prompt.isCancellable = true;
    					prompt.title = "Link to which draft?";
    					prompt.addButton("Insert Link");
    					prompt.addSelect("select", "", draftTitles, [draftTitles[0]], false);
    					var didSelect = prompt.show();
    					
    					var chosenDraftTitle = prompt.fieldValues["select"][0];
    					if (didSelect) {
    						var draftIndex = draftTitles.indexOf(chosenDraftTitle);
    						chosenDraft = foundDrafts[draftIndex];
    						
    					} else {
    						if (!editor.isActive) {
    							editor.activate();
    						}
    					}
    				}
    				
    				if (chosenDraft != null) {
    					// We don't insert the opening delimiter because it's already there.
    					var linkToInsert = chosenDraft.displayTitle + "]]";
    					
    					// Insert link, either replacing selection or relevant text before insertion point.
    					partialTitleRange[0] += selectedLineRange[0]; // make document-relative
    					
    					editor.setSelectedRange(partialTitleRange[0], partialTitleRange[1]);
    					editor.setSelectedText(linkToInsert); // avoid the setTextInRange() scrolling bug
    					//editor.setTextInRange(partialTitleRange[0], partialTitleRange[1], linkToInsert);
    					
    					editor.setSelectedRange(minRange(partialTitleRange) + linkToInsert.length, 0);
    					
    					editor.save();
    					draft.update();
    					if (!editor.isActive) {
    						editor.activate();
    					}
    					
    				}
    			} else {
    				app.displayInfoMessage("No matching drafts found.");
    			}
    		}
    		
    	} else {
    		app.displayInfoMessage("Couldn't find start of link ( [[ ).");
    	}
    	
    }
    

Options

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