Expander: Search and include list of notes

Gif presentation

v0.1

How to use

  1. Type {{tag:#tag}} in your note.
  2. Put a cursor on that line
  3. Open command palette and find “Expander:” commands (you can attach hotkeys in settings menu)

It should remove the {{tag:#tag}} line and add notes which was found.

Notes

  • The expander do not do the current file title exclusion. Please, be careful and remove the embedded self note before running preview mode (it can cause a recursion). Was fixed in v0.2. Thanks @GreenChocho for testing.

Install

  • You need Obsidian v0.9.7+
  • Get Latest release
  • Extract files and place them to your vault’s plugins folder: /.obsidian/plugins/
  • Reload Obsidian

For version below 0.3.0
To enable the plugin run this snippet in your console:

v0.2.1
!function() {
	const DELAY = 2000
	const config = [
		{
			id: 'editor:expandEmbeds',
			name: 'Expander: embed',
			format: e => '![[' + e + ']]'
		},
		{
			id: 'editor:expandLinks',
			name: 'Expander: links',
			format: e => '[[' + e + ']]'
		},
		{
			id: 'editor:expandList',
			name: 'Expander: list of links',
			format: e => '- [[' + e + ']]'
		},
		{
			id: 'editor:expandTODO',
			name: 'Expander: list of TODO',
			format: e => '- [ ] [[' + e + ']]'
		},
	]

	function reformatLinks(links, mapFunc) {
		return links.map(e => e.file.name)
	  		.filter(e => app.workspace.activeLeaf.view.file.name !== e)
	  		.map(mapFunc).join('\n')	
  	}

  	function getLastLineNum(doc, line = 0) {
  		const lineNum = line === 0 
	  		? doc.getCursor().line 
	  		: line

  		if (doc.lineCount() === lineNum) {
  			return doc.getCursor().line + 1
  		}

  		return doc.getLine(lineNum) === '---' 
  			? lineNum
  			: getLastLineNum(doc, lineNum + 1)
  	}

	function initExpander(mapFunc) {
			// Search files
			const search = query => app.globalSearch.openGlobalSearch(query)
			const getFoundFilenames = (mapFunc, callback) => {
			  const searchLeaf = app.workspace.getLeavesOfType('search')[0]
			  searchLeaf.open(searchLeaf.view)
			  .then(view => setTimeout(()=> {
			  	const result = reformatLinks(view.dom.resultDoms, mapFunc)
			  	callback(result)
			  }, DELAY))
			}
			const cmDoc = app.workspace.activeLeaf.view.sourceMode.cmEditor.doc

			const hasFormulaRegexp = /^\{\{.+\}\}$/
			const curNum = cmDoc.getCursor().line
			const curText = cmDoc.getLine(curNum)

			if (!hasFormulaRegexp.test(curText)) {
				return
			}

			const isEmbed = cmDoc.getLine(curNum - 1) === '```expander'
				&& cmDoc.getLine(curNum + 1) === '```'

			const fstLineNumToReplace = isEmbed 
				? curNum - 1 
				: curNum
			const lstLineNumToReplace = isEmbed
				? getLastLineNum(cmDoc)
				: curNum

			const searchQuery = curText.replace('{{', '').replace('}}', '')
			const embedFormula = '```expander\n' +
				'{{' + searchQuery + '}}\n' +
				'```\n'

			const log = content => console.log(content) || content

			const replaceLine = content => cmDoc.replaceRange(log(embedFormula + content + '\n\n---'), 
					{line: fstLineNumToReplace, ch: 0},
					{line: lstLineNumToReplace, ch: cmDoc.getLine(lstLineNumToReplace).length}
				)


			search(searchQuery)
			getFoundFilenames(mapFunc, replaceLine)
	}

	config.forEach(e => {
		app.commands.addCommand({
		    id: e.id,
		    name: e.name,
		    callback: () => initExpander(e.format),
		    hotkeys: []
		})
	})
}()

v0.2
!function() {
	const DELAY = 2000
	const config = [
		{
			id: 'editor:expandEmbeds',
			name: 'Expander: embed',
			format: e => '![[' + e + ']]'
		},
		{
			id: 'editor:expandLinks',
			name: 'Expander: links',
			format: e => '[[' + e + ']]'
		},
		{
			id: 'editor:expandList',
			name: 'Expander: list of links',
			format: e => '- [[' + e + ']]'
		},
		{
			id: 'editor:expandTODO',
			name: 'Expander: list of TODO',
			format: e => '- [ ] [[' + e + ']]'
		},
	]

	function reformatLinks(links, mapFunc) {
		return links.map(e => e.file.name)
	  		.filter(e => app.workspace.activeLeaf.view.file.name !== e)
	  		.map(mapFunc).join('\n')	
  	}

  	function getLastLineNum(cmInst, line = 0) {
  		const lineNum = line === 0 
	  		? cmInst.getCursor().line 
	  		: line

  		if (cmInst.lastLine() === lineNum) {
  			return cmInst.getCursor().line + 1
  		}

  		return cmInst.getLine(lineNum) === '---' 
  			? lineNum
  			: getLastLineNum(cmInst, lineNum + 1)
  	}

	function initExpander(mapFunc) {
			// Search files
			const search = query => app.globalSearch.openGlobalSearch(query)
			const getFoundFilenames = (mapFunc, callback) => {
			  const searchLeaf = app.workspace.getLeavesOfType('search')[0]
			  searchLeaf.open(searchLeaf.view)
			  .then(view => setTimeout(()=> {
			  	const result = reformatLinks(view.dom.resultDoms, mapFunc)
			  	callback(result)
			  }, DELAY))
			}
			const cmDoc = app.workspace.activeLeaf.view.sourceMode.cmEditor.doc

			const hasFormulaRegexp = /^\{\{.+\}\}$/
			const curNum = cmDoc.getCursor().line
			const curText = cmDoc.getLine(curNum)

			if (!hasFormulaRegexp.test(curText)) {
				return
			}

			const isEmbed = cmDoc.getLine(curNum - 1) === '```expander'
				&& cmDoc.getLine(curNum + 1) === '```'

			const fstLineNumToReplace = isEmbed 
				? curNum - 1 
				: curNum
			const lstLineNumToReplace = isEmbed
				? getLastLineNum(cmDoc)
				: curNum

			const searchQuery = curText.replace('{{', '').replace('}}', '')
			const embedFormula = '```expander\n' +
				'{{' + searchQuery + '}}\n' +
				'```\n'

			const log = content => console.log(content) || content

			const replaceLine = content => cmDoc.replaceRange(log(embedFormula + content + '\n\n---'), 
					{line: fstLineNumToReplace, ch: 0},
					{line: lstLineNumToReplace, ch: cmDoc.getLine(lstLineNumToReplace).length}
				)


			search(searchQuery)
			getFoundFilenames(mapFunc, replaceLine)
	}

	config.forEach(e => {
		app.commands.addCommand({
		    id: e.id,
		    name: e.name,
		    callback: () => initExpander(e.format),
		    hotkeys: []
		})
	})
}()

v0.1
!function() {
	const DELAY = 2000
	const config = [
		{
			id: 'editor:expandEmbeds',
			name: 'Expander: embed',
			format: e => '![[' + e + ']]'
		},
		{
			id: 'editor:expandLinks',
			name: 'Expander: links',
			format: e => '[[' + e + ']]'
		},
		{
			id: 'editor:expandList',
			name: 'Expander: list of links',
			format: e => '- [[' + e + ']]'
		},
		{
			id: 'editor:expandTODO',
			name: 'Expander: list of TODO',
			format: e => '- [ ] [[' + e + ']]'
		},
	]

	function initExpander(mapFunc) {
			// Search files
			const search = query => app.globalSearch.openGlobalSearch(query)
			// const getFoundFilenames = () => Array.from(document.querySelectorAll('.mod-global-search .search-result-file-title>span:nth-child(2)')).map(e => e.textContent)
			const getFoundFilenames = (mapFunc, callback) => {
			  const searchLeaf = app.workspace.getLeavesOfType('search')[0]
			  searchLeaf.open(searchLeaf.view)
			  .then(view => setTimeout(()=> {
			  	callback(view.dom.resultDoms.map(e => e.file.name).map(mapFunc).join('\n'))
			  }, DELAY))
			}
			const doc = app.workspace.activeLeaf.view.sourceMode.cmEditor.doc
			const curLineNum = doc.getCursor().line

			const lineContent = doc.getLine(curLineNum)

			if (!/^\{\{.+\}\}$/.test(lineContent)) {
				return
			}

			const searchQuery = lineContent.replace('{{', '').replace('}}', '')
			const replaceLine = content => doc.replaceRange(content, {line: curLineNum, ch: 0}, {line: curLineNum, ch: lineContent.length})

			search(searchQuery)
			getFoundFilenames(mapFunc, replaceLine)
	}

	config.forEach(e => {
		app.commands.addCommand({
		    id: e.id,
		    name: e.name,
		    callback: () => initExpander(e.format),
		    hotkeys: []
		})
	})
}()
 

What’s next

I see potential for improvements like:

  • Done in 0.2 Save snippet and update results on command trigger
  • Expand all snippets on page by command
  • Fix hacky search implementation when Search API will be provided

Latest changes

0.3
Available for official API Alpha

0.2.1

  • [FIXED] Skips check for --- if it’s last line in file.

0.2

  • Saves a formula snippet and wraps it into reusable code block
    ```extender
    {{tag:#tag}}
    ```
  • You can use snippet to “refresh” already added search results
  • You can hide snippet code block from preview using CSS snippet below
.language-extender {
    display: none;
}
  • [FIXED] Remove parent note from search results

0.1

  • Proof of concept added
13 Likes

Really looking forward to the official API + @mrjackphil!

A question: this is one-way, right? There’s no easy way to “refresh” the list outside of deleting the expanded list and replacing the expansion shortcode? Not a big deal, just making sure.

Edit: got it to work, and yes, no clear ability to return. It would be neat to alternate between the “expand search” query on edit and the results on preview, but I realize that that’s probably beyond the bounds of what’s possible without API access.

It is really cool though. Thanks!

Edit 2: Oddly it doesn’t seem to return all of the possible results for me. Only the first three that appear in Search. Hmm!

My suggestion is that maybe the way to Refresh is to give an option to leave the {{tag:#tag}} there as a bullet. Then expand the search results as sub-bullets. Then add an option to delete all sub-bullets and pasted in new list (or check and paste in new notes link only would be better).

3 Likes

@ryanjamurphy @GreenChocho

Please play with const delay = 5000 line where 5000 equals to 5s of waiting. In initial snippet it was 1000 so it may tried to extend not all search results.

There is no search API inside of Obsidian, so it’s a very hacky way to use search functionality inside of Obsidian.

It basically:

  • runs a usual search with line inside of brackets
  • waits for a delay time (didn’t find an event or something like that to execute the code right after search)
  • parse HTML and get files

EDIT: Found an issue. Trying to find workaround.

1 Like

Uploaded fixed version. Should work better. Still DELAY thing there but I’m closer to figure out how to fix this one.

2 Likes

In 0.9.3 version this functionality will be added, so no need for that plugin after that.

EDIT: But there is just copy search list functionality in 0.9.3. I think I can add “refresh” option for current plugin so it would be more useful.

2 Likes

Just to illustrate:

0.2

  • It adds --- at the end of the search result. Plugin uses this to understand which part to replace when new search results arrived.
  • Saves a formula snippet and wraps it into reusable code block
    ```extender
    {{tag:#tag}}
    ```
  • You can use snippet to “refresh” already added search results
  • You can hide snippet code block from preview using CSS snippet below
.language-extender {
    display: none;
}
  • [FIXED] Remove parent note from search results
2 Likes

0.2.1

  • [FIXED] Skips check for — if it’s last line in file.
2 Likes

I’ve been loving this plugin, is there a plan to make it available for the official API? Thanks!

1 Like

And now for official API

5 Likes

Are there any users of Text Expand plugin? I researched obsidian plugins and found a lot of plugins which do the same or almost the same thing.

Data-view plugin as an example which does the similar job.

So the question is - should we have a Text Expand at all? I’m still getting GitHub issues from people. I’m going to fix as much as I can when I’ll be able.

So people using it but I’m not sure why they choosing Text Expand over other solutions?

It sounds like I want to abandon the project but that’s not the reason. I want to find out what the main purpose of the plugin for users and focus on that. Or remove it from Obsidian Plugin and help others to improve solutions which does similar job or better.

1 Like

I think the Text Expand and Dataview still have unique offering. For me, i would still like to keep Text Expand.
Dataview is a great tool for seeing the overall picture, but it only work with preview and process YAML attributes.
While Text Expand return notes with search term in the note itself.
Furthermore, I think Text Expander would be very powerful if Obisidian return heading/block if it does get implemented (Copy Search Results: Add line/block/section function)

1 Like

I’m using it. It is very useful to display files & lines to create MOCs. Don’t stop the plugin! Thanks! :grin:

2 Likes

Storing results as opposed to searching them in real time at every search is the appeal to me, as well as using Obsidians own query language.
But I don’t use any plugins yet, because I’m looking for a stable one to restructure my notes (creating automatic indices) around its feature set. Expander lacks convenience for me to jump ship just yet. Looking forward to any future developments in this or any other plugin.

Just added “Expand all” functionality which should expand all blocks in the file. One after another.

@Jeffurry I saw your post here.

Can I help you with the Expand plugin?

Thanks @mrjackphil - your generous offer prompted me to have another go - and I managed to get a result! If I run into difficulties integrating the result into my workflow, I may take advantage of you later - if that is OK.
Cheers

1 Like

Sure. Ping me if you need any help. Here or in Discord.

Added ability to extract the headings


and blocks

Also, it will not open or toggle tabs on the left panel

2 Likes