number-headings-obsidian icon indicating copy to clipboard operation
number-headings-obsidian copied to clipboard

Update the reference to the heading

Open rainsia opened this issue 3 years ago • 21 comments

I have a lot of wiki links to headings. After I updated the heading numbers with this plugin, the links break.

Is it possible to update all the references when updating the heading numbers. Just like the official obsidian context menu item "rename this heading".

rainsia avatar Dec 22 '21 11:12 rainsia

This would be really an awesome feature.

+1 for this feature.

sidahmed-malaoui avatar Jan 21 '22 12:01 sidahmed-malaoui

+1 sincerely wish this improvement to be introduced

Maxlinn avatar Feb 11 '22 08:02 Maxlinn

I tried to implement this improvement but found it harder than i thought.

This auto-numbering plugin uses editor.transaction([]{text, from, to}) to directly change the text of the heading, not considering the hierarchy of headings.

I looked up official api doc of obsidian, but did not find any function like "changeHeading" which expected to change the text of heading and trigger the update references of this heading.

Since obsidian is not an open-souce software, i cannot discover how internal "rename this heading" in right-click menu works.

I looked through the code of another plugin obsidian-filename-heading-sync which also modifies headings. It does nothing to do with the editor, just uses obsidian api to open the file, modify lines, and use obsidian api to write it back(this.app.vault.modify(file, text)). However, it does trigger the update of references of the headings. I don't think this solution is graceful when the document is too large, but it works for the most of the time.

Maxlinn avatar Feb 13 '22 05:02 Maxlinn

This has been an issue from the beginning. It does need addressing because one ends up with a lot of broken links in the course of time.

I am not sure, but I get the impression Kevin has abandoned this plug-in because he has not been active with any of the issues for quite a while. This particular one was raised in December last year, but Kevin has not reacted to any of the comments.

Does anyone have a workaround?

DutchPete avatar May 17 '22 06:05 DutchPete

I'm still here, just slow to respond. I'll look into it!

onlyafly avatar May 30 '22 19:05 onlyafly

Hi @onlyafly. I think @Maxlinn's suggestion is feasible. Updating the reference to the heading in the vault could be a manual command or option which helps a lot. Since this command is manual waiting is acceptable.

Notes are borned to be linked. Without updating reference to the heading Number headings plugin could not be that useful. I hope this feature could be added. Thanks. 😃

hustrjh avatar Jan 16 '23 15:01 hustrjh

I'm actively researching this issue now and hope to have a solution soon!

In case you are interested, I'm looking for help on the Obsidian developer forums at https://forum.obsidian.md/t/api-to-access-the-rename-heading-functionality/52803

onlyafly avatar Jan 21 '23 10:01 onlyafly

@onlyafly Sincerely thanks to you. ❤️

hustrjh avatar Jan 22 '23 04:01 hustrjh

Hi, @onlyafly. There's another method to solve reference problem. I've got the idea from plugin called Visually numbered heading.

So Number headings plugin could introduce front matter attribute or even setting option to add heading prefix visually or trully. With various settings of number prefix Number headings would be super convenient.

I hope this could help. :smiley:

hustrjh avatar Feb 17 '23 01:02 hustrjh

@onlyafly I was using your plugin, but broken links got me really sad recently 😭 So I looked to what I can to about it. Then I found out, that in my workspace, Rename this heading... option in context menu actually renames all links to it!

image

BUT... then I then tested it in new workspace and it was not working here... So I found out, that plugin Find orphaned files and broken links was doing it!

image image image

It does not update text of the link, but it does update the link!

Hopefully this will help find a way to implement this 🙏

Nikolai2038 avatar Oct 08 '23 12:10 Nikolai2038

Thanks! I'll look into it

onlyafly avatar Oct 08 '23 16:10 onlyafly

Can confirm - that would be a great enhancement!

AndreiPashkin avatar Nov 21 '23 16:11 AndreiPashkin

This would be a great feature.

Idea: rather than changing all the references, which is ugly as well as tricky to do, couldn't create an anchor that would work like the original heading?

So (optionally) instead of transforming

## Foo Header

into

## X.Y Foo Header

transform it into

## X.Y Foo Header
^foo-header

Then you can use [[#^foo-header]] (this is the kebab-cased version of the header) to refer to the anchor, and it won't change when the numbers change.

shaunc avatar Dec 18 '23 21:12 shaunc

@onlyafly I was using your plugin, but broken links got me really sad recently 😭 So I looked to what I can to about it. Then I found out, that in my workspace, Rename this heading... option in context menu actually renames all links to it!

BUT... then I then tested it in new workspace and it was not working here... So I found out, that plugin Find orphaned files and broken links was doing it!


Heyha ! This actually works without any plugin ! I just tested it in the obsidian sandbox, and it works ! It updates all links and linked sections ! So it's probably a setting in obsidian or a new update I'm not aware of? Anyway thanks for the pointer, with a shortcut in place it will greatly reduce linked section rot !

KalyaSc avatar Jan 10 '24 14:01 KalyaSc

This actually works without any plugin

I can confirm, maybe it worked and I didn't check it properly, or maybe they actually implemented this idea. I think all that needs to be done now is to get this plugin to use this Rename this header... feature @onlyafly 🙏🏻

Nikolai2038 avatar Feb 19 '24 12:02 Nikolai2038

Hey @onlyafly, here is the source code of how Obsidian updates all references of headings. I found them in debugger. They are easy to find if you discover the place of fsPromises.writeFile.

I think fileManager.iterateAllRefs is the function you need.

part1

t.prototype.submit = function(e) {
                return v(this, void 0, void 0, (function() {
                    var t, n, i, r, o, a, s, l, c, u, h, p, d, f, m, g, v, b, w, k, C;
                    return y(this, (function(y) {
                        switch (y.label) {
                        case 0:
                            return (t = this.getError(e)) ? (qS(this.inputEl, t, {
                                placement: "right"
                            }),
                            [2, !0]) : (i = (n = this).file,
                            r = n.app,
                            o = r.fileManager,
                            a = r.vault,
                            s = this.getChanges(e),
                            0 !== (l = s.count()) ? [3, 1] : (this.replaceInEditor(e),
                            [3, 6]));
                        case 1:
                            return (c = s.get(i.path)) && c.length > 0 ? (s.removeKey(i.path),
                            u = this.replaceInFile(e),
                            h = {
                                link: "",
                                original: "",
                                position: {
                                    start: {
                                        line: 0,
                                        col: 0,
                                        offset: u.start
                                    },
                                    end: {
                                        line: 0,
                                        col: 0,
                                        offset: u.end
                                    }
                                }
                            },
                            [4, a.process(i, (function(e) {
                                var t = c;
                                return t.push({
                                    sourcePath: i.path,
                                    change: u.text,
                                    reference: h
                                }),
                                DR(e, t)
                            }
                            ))]) : [3, 3];
                        case 2:
                            return y.sent(),
                            [3, 4];
                        case 3:
                            this.replaceInEditor(e),
                            y.label = 4;
                        case 4:
                            return [4, o.updateInternalLinks(s)];
                        case 5:
                            y.sent(),
                            p = zf.nouns.linkWithCount({
                                count: l
                            }),
                            d = zf.nouns.fileWithCount({
                                count: Object.keys(s.data).length
                            }),
                            f = zf.dialogue.msgUpdatedLinks({
                                links: p,
                                files: d
                            }),
                            new hF(f),
                            y.label = 6;
                        case 6:
                            for (m = o.linkUpdaters,
                            g = 0,
                            v = Object.values(m); g < v.length; g++)
                                b = v[g],
                                w = this.getCustomReplacements(e),
                                k = w.oldSubpath,
                                C = w.newSubpath,
                                b.renameSubpath(this.file, k, C);
                            return this.close(),
                            [2]
                        }
                    }
                    ))
                }
                ))
            }

part2

            t.prototype.getError = function(e) {
                return "" === e ? "Cannot be empty" : null
            }
            ,
            t.prototype.getChanges = function(e) {
                var t = this.file
                  , n = this.app
                  , i = n.metadataCache
                  , r = new it
                  , o = cS(this.oldHeading).toLowerCase()
                  , a = uS(e);
                return n.fileManager.iterateAllRefs((function(e, n) {
                    var s = nS(n.link)
                      , l = s.path
                      , c = s.subpath;
                    c && (cS(c.substring(1)).toLowerCase() === o && i.getFirstLinkpathDest(l, e) === t && r.add(e, {
                        sourcePath: e,
                        reference: n,
                        change: AR(n, l + "#" + a)
                    }))
                }
                )),
                r
            }
            ,
            t.prototype.replaceInEditor = function(e) {
                var t = this.cursor
                  , n = this.editor
                  , i = t.line
                  , r = n.getLine(i)
                  , o = this.replaceHeadingText(r, e);
                n.setLine(i, o)
            }
            ,
            t.prototype.replaceInFile = function(e) {
                var t = this.cursor
                  , n = this.editor
                  , i = t.line
                  , r = n.posToOffset(om(i))
                  , o = n.getLine(i)
                  , a = this.replaceHeadingText(o, e);
                return {
                    start: r,
                    end: r + o.length,
                    text: a
                }
            }
            ,
            t.prototype.replaceHeadingText = function(e, t) {
                return e.replace(/^(#{1,6} ).*/m, (function(e, n) {
                    return n + t
                }
                ))
            }
            ,
            t.prototype.getCustomReplacements = function(e) {
                return {
                    oldSubpath: cS(this.oldHeading).toLowerCase(),
                    newSubpath: uS(e)
                }
            }

longguzzz avatar Apr 20 '24 02:04 longguzzz

@onlyafly Somebody has implemented renaming headings in quickadd:https://forum-zh.obsidian.md/t/topic/30546/1 app.workspace.activeEditor app.fileManager.iterateAllRefs app.metadataCache.getFirstLinkpathDest app.vault.process app.metadataCache.getFileCache(file).headings

let rgx = "/.*/",
  form = "`$&-${i+1}`";
const prompt = (str, holder, value) =>
    this.quickAddApi.inputPrompt(str, holder, value),
  { editor: Editor, file } = app.workspace.activeEditor,
  lv = await prompt("级别", "1-6", "1-6"),
  confirm = async (file) => {
    let r = [],
      repChan = (raw, isRef) => {
        chans.map((chan) => {
          let rgx = new RegExp(
            isRef ? `\\[\\[.+?#${chan[0]}.*?]]` : `^#{${chan[2]}} ${chan[0]}$`,
            "gm"
          );
          raw = raw.replace(rgx, (m) => m.replace(chan[0], chan[1]));
        });
        return raw;
      },
      test = chans[0].heading;
    do {
      rgx = await prompt(`正则`, rgx, rgx);
      if (!rgx) return;
      form = await prompt(`替换`, form, form);
    } while (!form);
    if (
      await this.quickAddApi.yesNoPrompt(
        test,
        [test].map((p, i) => test.replace(eval(rgx), eval(form)))
      )
    ) {
      chans = chans
        .filter((p) => p.level >= lv.slice(0, 1) && p.level <= lv.slice(-1))
        .map((p, i) => [
          p.heading,
          p.heading.replace(eval(rgx), eval(form)),
          p.level,
        ]);
      app.fileManager.iterateAllRefs((rPath, rbj) => {
        if (
          app.metadataCache.getFirstLinkpathDest(rbj.link.split("#")[0], rPath)
            ?.path == file.path
        )
          r.includes(rPath) || r.push(rPath);
      });
      r.map(
        async (rPath) =>
          await app.vault.process(
            app.vault.getAbstractFileByPath(rPath),
            (raw) => repChan(raw, 1)
          )
      );
      setTimeout(
        () => Editor.replaceSelection(repChan(Editor.getSelection(), 0)),
        50
      );
    } else confirm(file);
  },
  hds = app.metadataCache.getFileCache(file).headings;
if (!hds) return;
let chans = hds.filter(
  (hd) =>
    hd.position.end.line >= Editor.getCursor("from").line &&
    hd.position.end.line <= Editor.getCursor("to").line
);
if (!chans[0]) {
  new Notice("无选中", 1000);
  return;
}
confirm(file);

longguzzz avatar Apr 27 '24 04:04 longguzzz

Hi, @onlyafly . I have the same issue and feel sad to get almost all my links broken 😭. Is this about to be solved? Thanks a lot.

ChenyuZhang97 avatar Aug 08 '24 04:08 ChenyuZhang97

I think with all the ideas above, this should not be too difficult for me to implement. I just need to find some time to devote a few solid hours to implement and debug it!

onlyafly avatar Aug 08 '24 11:08 onlyafly

Thank you! I'm very much looking forward to the new version of the plugin.😊

ChenyuZhang97 avatar Aug 08 '24 13:08 ChenyuZhang97

Looking forward to this new feature.

phooeny avatar Sep 15 '24 01:09 phooeny