Skip to content

Commit 447e5c9

Browse files
committed
feat(commands): join via space, commas & new lines
1 parent 041c5c1 commit 447e5c9

File tree

3 files changed

+176
-4
lines changed

3 files changed

+176
-4
lines changed

src/app.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BlockEntity, SettingSchemaDesc } from '@logseq/libs/dist/LSPlugin.user'
22

33
import {
4-
ICON, editNextBlockCommand, editPreviousBlockCommand, reverseBlocksCommand,
4+
ICON, editNextBlockCommand, editPreviousBlockCommand, joinBlocksCommand, reverseBlocksCommand,
55
shuffleBlocksCommand, sortBlocksCommand, splitBlocksCommand, splitByLines, splitByParagraphs, splitByWords, toggleAutoHeadingCommand
66
} from './commands'
77
import { getChosenBlocks, p, scrollToBlock } from './utils'
@@ -99,7 +99,7 @@ async function main() {
9999
}, (e) => toggleAutoHeadingCommand({togglingBasedOnFirstBlock: true}) )
100100

101101

102-
// Splitting & Joining
102+
// Splitting
103103
logseq.App.registerCommandPalette({
104104
label: ICON + ' Split by words', key: 'split-1-by-words',
105105
// @ts-expect-error
@@ -155,6 +155,64 @@ async function main() {
155155
))
156156

157157

158+
// Joining
159+
logseq.App.registerCommandPalette({
160+
label: ICON + ' Join via spaces', key: 'join-1-spaces',
161+
// @ts-expect-error
162+
keybinding: {},
163+
}, (e) => joinBlocksCommand(
164+
false,
165+
(root, level, children) => (root ? root + ' ' : '') + children.join(' '),
166+
))
167+
168+
logseq.App.registerCommandPalette({
169+
label: ICON + ' Join selected together via commas (with respect to root block)', key: 'join-2-commas',
170+
// @ts-expect-error
171+
keybinding: {},
172+
}, (e) => joinBlocksCommand(
173+
false,
174+
(root, level, children) => {
175+
const prefix = root ? root + (level === 0 ? ': ' : ', ') : ''
176+
return prefix + children.join(', ')
177+
},
178+
))
179+
logseq.App.registerCommandPalette({
180+
label: ICON + ' Join selected independently via commas (with respect to root block)', key: 'join-3-commas-independently',
181+
// @ts-expect-error
182+
keybinding: {},
183+
}, (e) => joinBlocksCommand(
184+
true,
185+
(root, level, children) => {
186+
const prefix = root ? root + (level === 0 ? ': ' : ', ') : ''
187+
return prefix + children.join(', ')
188+
},
189+
))
190+
191+
logseq.App.registerCommandPalette({
192+
label: ICON + ' Join via new lines', key: 'join-4-lines',
193+
// @ts-expect-error
194+
keybinding: {},
195+
}, (e) => joinBlocksCommand(
196+
false,
197+
(root, level, children) => (root ? root + '\n' : '') + children.join('\n'),
198+
))
199+
200+
logseq.App.registerCommandPalette({
201+
label: ICON + ' Join via new lines (keep nested structure)', key: 'join-5-lines-nested',
202+
// @ts-expect-error
203+
keybinding: {},
204+
}, (e) => joinBlocksCommand(
205+
false,
206+
(root, level, children) => (root ? root + '\n' : '') + children.join('\n'),
207+
(content, level) => {
208+
if (level <= 1)
209+
return content
210+
const prefix = '* '
211+
return '\t'.repeat(level - 1) + prefix + content
212+
},
213+
))
214+
215+
158216
// Navigation
159217
logseq.App.registerCommandPalette({
160218
label: ICON + ' Go to (↖︎) parent block', key: 'edit-block-1-deep-dive-out',

src/commands.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import '@logseq/libs'
22

3-
import { PropertiesUtils, filterOutChildBlocks, getChosenBlocks, insertBatchBlockBefore, isWindows, sleep, transformSelectedBlocksCommand, unique, walkBlockTreeAsync } from './utils'
3+
import {
4+
PropertiesUtils, ensureChildrenIncluded, filterOutChildBlocks, getBlocksWithReferences, getChosenBlocks, insertBatchBlockBefore,
5+
isWindows, reduceBlockTree, reduceTextWithLength, sleep, transformBlocksTreeByReplacing, transformSelectedBlocksWithMovements, unique, walkBlockTree, walkBlockTreeAsync,
6+
} from './utils'
47
import { BlockEntity, IBatchBlock } from '@logseq/libs/dist/LSPlugin'
58

69

@@ -314,6 +317,116 @@ export async function splitBlocksCommand(
314317
}
315318
}
316319

320+
export async function joinBlocksCommand(
321+
independentMode: boolean,
322+
joinAttachCallback: (root: string, level: number, children: string[]) => string,
323+
joinMapCallback?: (content: string, level: number) => string,
324+
) {
325+
let [ blocks, isSelectedState ] = await getChosenBlocks()
326+
if (blocks.length === 0)
327+
return
328+
329+
blocks = unique(blocks, (b) => b.uuid)
330+
blocks = await Promise.all(
331+
blocks.map(async (b) => {
332+
return (
333+
await logseq.Editor.getBlock(b.uuid, {includeChildren: true})
334+
)!
335+
})
336+
)
337+
blocks = filterOutChildBlocks(blocks)
338+
339+
if (blocks.length === 0)
340+
return
341+
if (blocks.length === 1)
342+
if (!blocks[0].children || blocks[0].children.length === 0)
343+
return // nothing to join
344+
345+
let noWarnings = true
346+
for (const block of blocks) {
347+
// it is important to check if any block in the tree has references
348+
// (Logseq replaces references with it's text)
349+
let blocksWithReferences = await getBlocksWithReferences(block)
350+
351+
// root can have references (in independent mode), others — not
352+
const blocksWithReferences_noRoot = blocksWithReferences.filter((b) => b.uuid !== block.uuid)
353+
block._rootHasReferences = blocksWithReferences.length !== blocksWithReferences_noRoot.length
354+
355+
if (independentMode || blocks.length === 1)
356+
blocksWithReferences = blocksWithReferences_noRoot
357+
358+
if (blocksWithReferences.length !== 0) {
359+
const html = blocksWithReferences.map((b) => {
360+
let content = PropertiesUtils.deleteAllProperties(b.content)
361+
content = reduceTextWithLength(content, 35)
362+
return `[:li [:i "${content}"]]`
363+
})
364+
await logseq.UI.showMsg(
365+
`[:div
366+
[:b "${ICON} Join Blocks Command"]
367+
[:p "There are blocks that have references from another blocks: "
368+
[:ul ${html}]
369+
]
370+
[:p "Remove references or select only blocks without them."]
371+
]`,
372+
'warning',
373+
{timeout: 20000},
374+
)
375+
noWarnings = false
376+
continue
377+
}
378+
}
379+
380+
if (!noWarnings)
381+
return
382+
383+
function reduceTree(block: IBatchBlock, { startLevel }: { startLevel: number }) {
384+
const preparedTree = walkBlockTree(block, (b, level) => {
385+
const content = PropertiesUtils.deleteAllProperties(b.content)
386+
return joinMapCallback ? joinMapCallback(content, level) : content
387+
}, startLevel)
388+
389+
const reducedContent = reduceBlockTree(preparedTree, (b, level, children) => {
390+
if (children.length === 0)
391+
return b.content
392+
return joinAttachCallback(b.content, level, children)
393+
}, startLevel)
394+
395+
return reducedContent
396+
}
397+
398+
if (independentMode || blocks.length === 1) {
399+
for (const block of blocks) {
400+
if (!block.children || block.children.length === 0)
401+
continue // nothing to join
402+
403+
const reducedContent = reduceTree(block as IBatchBlock, {startLevel: 0})
404+
405+
if (block._rootHasReferences) {
406+
await logseq.Editor.updateBlock(block.uuid, reducedContent)
407+
for (const child of block.children!)
408+
await logseq.Editor.removeBlock((child as BlockEntity).uuid)
409+
} else {
410+
await insertBatchBlockBefore(block, {content: reducedContent, properties: block.properties})
411+
await logseq.Editor.removeBlock(block.uuid)
412+
}
413+
}
414+
} else {
415+
const top = blocks[0]
416+
const pseudoRoot: IBatchBlock = {content: '', children: blocks as IBatchBlock[]}
417+
const reducedContent = reduceTree(pseudoRoot, {startLevel: 0})
418+
419+
await insertBatchBlockBefore(top, {content: reducedContent, properties: top.properties})
420+
for (const block of blocks)
421+
await logseq.Editor.removeBlock(block.uuid)
422+
}
423+
424+
if (isSelectedState) {
425+
await sleep(20)
426+
await logseq.Editor.exitEditingMode()
427+
}
428+
}
429+
317430
// export async function magicSplitCommand() {
318431
// const [ blocks, isSelectedState ] = await getChosenBlocks()
319432
// if (blocks.length === 0) {

src/utils/logseq.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ export async function ensureChildrenIncluded(node: BlockEntity): Promise<BlockEn
246246
return (await logseq.Editor.getBlock(node.uuid, {includeChildren: true}))!
247247
}
248248

249-
export async function replaceChildrenBlocksInTree(
250249
export async function getBlocksWithReferences(root: BlockEntity): Promise<BlockEntity[]> {
251250
const blocksWithPersistedID = findPropertyInTree(root as IBatchBlock, PropertiesUtils.idProperty)
252251
const blocksAndItsReferences = (await Promise.all(
@@ -262,6 +261,8 @@ export async function getBlocksWithReferences(root: BlockEntity): Promise<BlockE
262261
return b
263262
})
264263
}
264+
265+
export async function transformBlocksTreeByReplacing(
265266
root: BlockEntity,
266267
transformChildrenCallback: (blocks: BlockEntity[]) => BlockEntity[],
267268
): Promise<BlockEntity | null> {

0 commit comments

Comments
 (0)