Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add feature to extract a heading with all of its sub-heading included #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This [Obsidian.md](https://obsidian.md) plugin fixes some problems of the core [
- [Heading/block links are not updated properly](https://forum.obsidian.md/t/note-composer-links-to-blocks-and-headers-should-be-updated-when-extracting/37534)
- Cannot undo extraction in the destination file

Currently, it only supports extraction of the current selection or heading.
Currently, it only supports extraction of the current selection or heading (with and without extracting sub-headings).
Use the core plugin for merging notes.

## Installation
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "better-note-composer",
"name": "Better Note Composer",
"version": "0.1.2",
"version": "0.2.1",
"minAppVersion": "1.3.5",
"description": "",
"author": "Ryota Ushio",
Expand Down
99 changes: 96 additions & 3 deletions src/extract.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BlockSubpathResult, CachedMetadata, Editor, FrontmatterLinkCache, HeadingSubpathResult, LinkCache, Loc, MarkdownView, PaneType, Reference, ReferenceCache, TFile, getLinkpath, parseLinktext, resolveSubpath } from 'obsidian';
import { BlockSubpathResult, CachedMetadata, Editor, FrontmatterLinkCache, HeadingSubpathResult, LinkCache, Loc, MarkdownView, Notice, PaneType, Reference, ReferenceCache, TFile, getLinkpath, parseLinktext, resolveSubpath } from 'obsidian';
import { TransactionSpec, Line } from '@codemirror/state';
import { EditorView } from '@codemirror/view';

import BetterNoteComposerPlugin from 'main';
import { BetterNoteComposerComponent, contains, getDisplayText, isEmbed, isFrontmatterLinkCache, isHeading, isReferenceCache, offsetToLoc, replaceSubstringByPos } from 'utils';
import { BetterNoteComposerComponent, contains, getDisplayText, getHeadingLevel, isEmbed, isFrontmatterLinkCache, isHeading, isReferenceCache, offsetToLoc, replaceSubstringByPos } from 'utils';
import { get } from 'http';


export interface InFileRange {
Expand Down Expand Up @@ -71,6 +72,60 @@ export class Extractor extends BetterNoteComposerComponent {

return this.extract(srcRange, dstFile, cm, paneType);
}

// Extract heading and all its subheadings
async extractHeadingRecursive(srcFile: TFile, editor: Editor, dstFile: TFile, paneType: PaneType | boolean): Promise<void> {
// @ts-ignore
const cm: EditorView = editor.cm;
const currentLine = cm.state.doc.lineAt(cm.state.selection.main.anchor);

let currentHeadingLine: Line | null = null;
let currentHeaderLevel: number | null = null;

currentHeadingLine = cm.state.doc.line(currentLine.number);
currentHeaderLevel = getHeadingLevel(currentHeadingLine.text);

if (!currentHeaderLevel) {
new Notice(`${this.plugin.manifest.name}: Cannot extract non-heading line recursively`);
throw Error(`${this.plugin.manifest.name}: Cannot extract non-heading line recursively`);
}

let nextHeadingLine: Line | null = null;
let inCodeBlock = false;
for (let i = currentLine.number + 1; i <= cm.state.doc.lines; i++) {
const line = cm.state.doc.line(i);
// Avoid matching with comments in a code block
if (line.text.startsWith('```')) {
inCodeBlock = !inCodeBlock;
}

if (inCodeBlock) continue;
if (!isHeading(line.text)) continue;

if (getHeadingLevel(line.text) <= currentHeaderLevel) {
nextHeadingLine = line;
break;
}
}

if (inCodeBlock) {
new Notice(`${this.plugin.manifest.name}: Cannot extract heading recursively inside a code block`);
throw Error(`${this.plugin.manifest.name}: Cannot extract heading recursively inside a code block`);
}

const numLines = cm.state.doc.lines;
const srcRange = {
file: srcFile,
start: currentHeadingLine
? offsetToLoc(editor, currentHeadingLine.from)
: { line: 0, col: 0, offset: 0 },
end: nextHeadingLine
? offsetToLoc(editor, nextHeadingLine.from - 1)
: { line: numLines - 1, col: cm.state.doc.line(numLines).length, offset: cm.state.doc.length }
};

return this.extract(srcRange, dstFile, cm, paneType);
}
}


Expand Down Expand Up @@ -177,12 +232,33 @@ export class ExtractionTask extends BetterNoteComposerComponent {
};

let replacement = '';
const heading = cache.headings?.find((heading) => contains(this.extraction.srcRange, heading.position));

const option = this.plugin.getReplacementText();
if (option !== 'none') {
const linkToExtraction = this.app.fileManager.generateMarkdownLink(this.extraction.dstFile, sourcePath);
let subpath = '';
let alias = '';
if (heading) {
if (this.plugin.getLinkToDestHeading()) {
subpath = '#' + heading.heading;
}

if (this.plugin.getUseHeadingAsAlias()) {
alias = heading.heading;
}
}

const linkToExtraction = this.app.fileManager.generateMarkdownLink(this.extraction.dstFile, sourcePath, subpath, alias);
replacement = option === 'link' ? linkToExtraction : '!' + linkToExtraction;
}

if (this.plugin.getKeepHeading()) {
if (heading) {
const headingText = "#".repeat(heading.level) + ' ' + heading.heading;
replacement = headingText + '\n\n' + replacement;
}
}

const replaceSrcRangeTransactionSpec: TransactionSpec = {
changes: {
from: this.extraction.srcRange.start.offset,
Expand Down Expand Up @@ -320,5 +396,22 @@ export class ExtractionTask extends BetterNoteComposerComponent {
return data;
});
}

if (this.plugin.getStayOnSourceFile()) {
await leaf.openFile(this.extraction.srcRange.file);
// Set cursor and scroll to the extracted position
// @ts-ignore
if (leaf.view instanceof MarkdownView) {
// @ts-ignore
const editor = leaf.view.editor;
const extractedLine = this.extraction.srcRange.start.line;
console.log(this.extraction)
console.log(extractedLine)
editor.setCursor({ line: extractedLine, ch: 0 });
editor.scrollIntoView( {
from: { line: extractedLine, ch: 0 }, to: { line: extractedLine, ch: 0 }
}, true);
}
}
}
}
37 changes: 36 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Extractor } from 'extract';
import { MarkdownFileChooserModal } from 'modals';
import { BetterNoteComposerEditorCommand } from 'commands';
import { BetterNoteComposerSettings, DEFAULT_SETTINGS, BetterNoteComposerSettingTab } from 'settings';

import { isHeading } from 'utils';

export default class BetterNoteComposerPlugin extends Plugin {
settings: BetterNoteComposerSettings;
Expand Down Expand Up @@ -63,6 +63,25 @@ export default class BetterNoteComposerPlugin extends Plugin {
});
}
}),
new BetterNoteComposerEditorCommand({
id: 'extract-heading-recursive',
name: 'Extract this heading recursively...',
checker: (editor, info) => {
// @ts-ignore
const cm: EditorView = editor.cm;
const currentLine = cm.state.doc.lineAt(cm.state.selection.main.anchor);
let currentHeadingLine = cm.state.doc.line(currentLine.number);

// Must click on the heading line to avoid matching with comments in a code block
return !!info.file && isHeading(currentHeadingLine.text);
},
executor: (editor, info) => {
const srcFile = info.file!;
showModalAndRun(srcFile, (dstFile, evt) => {
this.extractor.extractHeadingRecursive(srcFile, editor, dstFile, Keymap.isModEvent(evt))
});
}
}),
];

commands.forEach((command) => this.addCommand(command.toCommand()));
Expand All @@ -77,4 +96,20 @@ export default class BetterNoteComposerPlugin extends Plugin {
? (this.app.internalPlugins.plugins['note-composer'].instance.options.replacementText ?? 'link')
: this.settings.replacementText;
}

getStayOnSourceFile(): boolean {
return this.settings.stayOnSourceFile;
}

getKeepHeading(): boolean {
return this.settings.keepHeading;
}

getLinkToDestHeading(): boolean {
return this.settings.linkToDestHeading;
}

getUseHeadingAsAlias(): boolean {
return this.settings.useHeadingAsAlias;
}
}
52 changes: 52 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ const REPLACEMENT_TEXT = {

export interface BetterNoteComposerSettings {
replacementText: keyof typeof REPLACEMENT_TEXT;
stayOnSourceFile: boolean;
keepHeading: boolean;
linkToDestHeading: boolean;
useHeadingAsAlias: boolean;
}

export const DEFAULT_SETTINGS: BetterNoteComposerSettings = {
replacementText: 'same',
stayOnSourceFile: true,
keepHeading: true,
linkToDestHeading: true,
useHeadingAsAlias: true,
};

export class BetterNoteComposerSettingTab extends PluginSettingTab {
Expand All @@ -36,5 +44,49 @@ export class BetterNoteComposerSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
});
});

new Setting(this.containerEl)
.setName('Stay on source file')
.setDesc('Stay on the source file after extraction.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.stayOnSourceFile)
.onChange(async (value) => {
this.plugin.settings.stayOnSourceFile = value;
await this.plugin.saveSettings();
});
});

new Setting(this.containerEl)
.setName('Keep heading')
.setDesc('Keep the heading of the source file after extraction.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.keepHeading)
.onChange(async (value) => {
this.plugin.settings.keepHeading = value;
await this.plugin.saveSettings();
});
});

new Setting(this.containerEl)
.setName('Link to destination heading')
.setDesc('Link to the destination heading after extraction if using the "Link" option.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.linkToDestHeading)
.onChange(async (value) => {
this.plugin.settings.linkToDestHeading = value;
await this.plugin.saveSettings();
});
});

new Setting(this.containerEl)
.setName('Use heading as alias')
.setDesc('Use the heading as the alias if using the "Link" option.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.useHeadingAsAlias)
.onChange(async (value) => {
this.plugin.settings.useHeadingAsAlias = value;
await this.plugin.saveSettings();
});
});
}
}
4 changes: 4 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export function isHeading(line: string) {
return /^#{1,6} /.test(line);
}

export function getHeadingLevel(line: string) {
return line.match(/^(#{1,6}) /)?.[1].length ?? 0;
}

export function offsetToLoc(editor: Editor, offset: number): Loc {
const pos = editor.offsetToPos(offset);
return { line: pos.line, col: pos.ch, offset };
Expand Down