Markdownでカーソル位置の見出し名を表示するSublime Textプラグイン

# できること
カーソル位置から上に一番近い見出し(#記法)[^1]の文字をステータスバーに表示します。

# 動作例

![sb.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62354/13542f61-0cdb-6abf-5ac4-39a9db3c0740.jpeg)

# 作った理由

類似機能を探すことが難しかったため。

# 欲しかった理由

文章ではなく、たとえば人名やタスクなどを見出しでざっくり`# 上司`, `# A社`, `## A社営業部`などと分類して振り分けていくことがあり、数が多くなると見出しが見えなくなり検索で飛んだときなどにどの属性(見出し)か分からず上にスクロールするケースが出てきた。
管理方法の見直しより対症療法的にこの上スクロールでの確認を省くことにした。

# コード

https://github.com/khsk/ViewMarkdownHeading

[^1]: `---`や`====`による見出しには非対応。

```json:ViewMarkdownHeading.sublime-settings
{
    "target_syntaxs": ["Markdown", "MultiMarkdown"],
    "max_length": 30
}
```

```python:ViewMarkdownHeading.py
import sublime
import sublime_plugin
import os

class ViewMarkdownHeading(sublime_plugin.ViewEventListener):
    STATUS_KEY = 'md_heading'

    def __init__(self, view):
        super().__init__(view)
        self.ENABLED = False
        self.view.settings().add_on_change('syntax', self.is_target_file)
        self.is_target_file()


# バッファファイルでトリガーしないのでinitに任せる
#    def on_load_async(self):
#        print('on_load_async')

# 実用面で入力≒selectionの移動＝on_selection_modifiedなので、不満が出るまで置いておく
#    def on_modified_async(self):
#        print('mod')
#        self.main()

    def on_selection_modified_async(self):
        self.main()

    def is_target_file(self):
        syntax = os.path.splitext(os.path.basename(self.view.settings().get("syntax")))[0]

        targetSyntaxs = sublime.load_settings('ViewMarkdownHeading.sublime-settings').get('target_syntaxs')
        self.ENABLED = syntax in targetSyntaxs

    def reset_md_heading_key(self):
        self.view.erase_status(ViewMarkdownHeading.STATUS_KEY)

    def set_md_heading_key(self, str):
        self.view.set_status(ViewMarkdownHeading.STATUS_KEY, str[:sublime.load_settings('ViewMarkdownHeading.sublime-settings').get("max_length")])

    def main(self):
        if self.ENABLED == False:
            self.reset_md_heading_key()
            return

        sel = self.view.sel()
        carret = sel[0].begin()
        if carret == 0:
            self.reset_md_heading_key()
            return

        headings =  self.view.find_all('(?m)^#+\s*.*')

        if len(headings) == 0:
            self.reset_md_heading_key()
            return

        for i, heading in enumerate(headings):
            # カーソル位置より下の見出しがあれば、一つ前の見出しに属している
            if heading.begin() > carret:
                if i == 0:
                    self.reset_md_heading_key()
                    break
                self.set_md_heading_key(self.view.substr(headings[i - 1]).lstrip('#').lstrip())
                break
            # 最後まで属してなければ最後の見出しに
            if i + 1 == len(headings):
                self.set_md_heading_key(self.view.substr(headings[i]).lstrip('#').lstrip())

```

## うまぶりポイント

全部のファイルに反応するのではなく(右下で設定する)特定のシンタックスファイルの場合にのみ動作させたかった。
シンタックスは拡張子の自動判定のほか、(特に新規未保存ファイルなどで)開いているファイルのシンタックスを動的に変更することができる。
幸い、Settingsクラスにonchangeイベントがあったのでシンタックスの変更時に処理するべきファイルかを再判定できた。

```py
self.view.settings().add_on_change('syntax', self.is_target_file)
```


# ステータスバーに表示した理由

ミニマップのように恒常的に表示するAPIがなさそうに見えたため。
コマンド呼び出しで一時表示する方法もあったが不精なので常時表示したかった。

# シンタックスの判定

`self.view.settings().get("syntax")`
では
`Packages/Markdown/Markdown.sublime-syntax`
といったパスで返ってくる。
また拡張子が`tmLanguage`のものもあり、今回はosモジュールを使ったやや冗長に見えるファイル名の抜き出し方法になった。

# 見出し判定

正規表現で行っているが、カーソルの真上の見出しを探す関係上、カーソル位置以降のテキストは不要。
しかしAPIではビューに対する`find`か`find_all`しかないため、全文検索している。
効率よくするならカーソル位置までの文字列を切り出し普通の正規表現で調べるべきだろうが、初めて触ることもありAPIの機能のほうを使用した。

# 開発プラグインの子ディレクトリ化

新規にプラグインを作る場合はメニューから
**Tools -> Developer-> New Plugin**
で雛形を作り、そのまま保存すると
**(ST3)\Data\Packages\User\\**
に保存される。
そのまま開発が進み、コンフィグなど複数ファイルを扱うようになると、ディレクトリを切り管理したくなる。
しかし、**User**の下にディレクトリを設置してもそのプラグインは読み込まれない。**User**直下でないと駄目なようだ。
なので、ディレクトリ単位で扱いたい場合はインストールするプラグインと同様にひとつ上、**(ST3)\Data\Packages\\** に設置するとよい。

[Sublime Text 3でプラグインを作るメモ - Qiita#ひな形の生成から実行まで](https://qiita.com/zaburo/items/2a579dfcc341c49452e9#%E3%81%B2%E3%81%AA%E5%BD%A2%E3%81%AE%E7%94%9F%E6%88%90%E3%81%8B%E3%82%89%E5%AE%9F%E8%A1%8C%E3%81%BE%E3%81%A7)

では省略か最初から **(ST3)\Data\Packages\test\\** に置くように書かれており、おそらく遅まきに理由を理解した。

# メニューからコンフィグファイルへのアクセス

既存のインストール済みプラグインの一部はメニューの
**Preferences -> Package Settings -> パッケージ名 ->**
からコンフィグファイルにアクセスできる。
しかしコンフィグファイルを作り自作プラグインを読み込んだだけではここには追加されない。
この処理はPackage Controlが管轄していると推測していたが、GitHub経由でのPackage Controlからのインストールでも追加されない。
結局、各プラグインが自主的にこの場所におのおのメニューを追加しているということが分かった。
つまりメニューを追加する`Main.sublime-menu`ファイルだ。

```json:Main.sublime-menu
[
    {
        "id": "preferences",
        "children": [
            {
                "id": "package-settings",
                "children": [
                    {
                        "caption": "ViewMarkdownHeading",
                        "mnemonic": "V",
                        "children": [
                            {
                                "caption": "Settings",
                                "mnemonic": "S",
                                "command": "open_file", "args": {
                                    "file": "${packages}/ViewMarkdownHeading/ViewMarkdownHeading.sublime-settings"
                                }
                            }
                        ]
                    }
                ]
            }
        ]
    }
]
```

既存のものからの改変のため[]で始まっているが、{}でもよいようだ。

# GitHubからのインストール

Package ControlはGitHubのURLに対応しておらず、URLを入力してもインストールされない。
しかしGitのリポジトリには対応しているため、リポジトリを追加すればインストールの候補に出る。
リポジトリ追加は
`Package Contorol: Add repository`
から。
`https://github.com/khsk/ViewMarkdownHeading.git`
などと入力して追加する。
ただPublishしない個人的なローカルな開発ならPackage Controlを経由する利点はさほどないかもしれない。


# 参考

* [Python | スライスを使って文字列の指定範囲の部分文字列を取得する](https://www.javadrive.jp/python/string/index11.html)
* [Pythonのクラス変数とインスタンス変数 | UX MILK](https://uxmilk.jp/41600)
* [How do I call a parent class's method from a child class in Python? - Stack Overflow](https://stackoverflow.com/questions/805066/how-do-i-call-a-parent-classs-method-from-a-child-class-in-python)
* [Linux SublimeText 3 で自作プラグイン - Qiita](https://qiita.com/narupo/items/99df2edf889630bcea2f)
* [Python | リストに指定した値と同じ要素が含まれているか確認する](https://www.javadrive.jp/python/list/index10.html)
* [Sublime Text API Reference（翻訳） - Qiita](https://qiita.com/TalesofFox/items/387ef41d37a3c54a83eb)
* [python - Get file syntax selection in sublime text 3 plugin - Stack Overflow](https://stackoverflow.com/questions/32740855/get-file-syntax-selection-in-sublime-text-3-plugin)
* [Sublime Text 3でプラグインを作るメモ - Qiita](https://qiita.com/zaburo/items/2a579dfcc341c49452e9)
* [API Reference – Sublime Text Documentation](https://www.sublimetext.com/docs/api_reference.html#sublime_plugin.ViewEventListener:ver-3.2)
* [Pythonのfor文でインデックスを同時に参照する：enumerate() | UX MILK](https://uxmilk.jp/8680)
* [sublimetext2 - Sublime Text 3: How to install plugins from Github - Stack Overflow](https://stackoverflow.com/questions/23026201/sublime-text-3-how-to-install-plugins-from-github)

# バックアップ
[Python | スライスを使って文字列の指定範囲の部分文字列を取得する Archive](http://web.archive.org/web/20220223122959/https://www.javadrive.jp/python/string/index11.html)
[Pythonのクラス変数とインスタンス変数 | UX MILK Archive](http://web.archive.org/web/20220223123017/https://uxmilk.jp/41600)
[How do I call a parent class's method from a child class in Python? - Stack Overflow Archive](http://web.archive.org/web/20220223123034/https://stackoverflow.com/questions/805066/how-do-i-call-a-parent-classs-method-from-a-child-class-in-python)
[Linux SublimeText 3 で自作プラグイン - Qiita Archive](http://web.archive.org/web/20220223123050/https://qiita.com/narupo/items/99df2edf889630bcea2f)
[Python | リストに指定した値と同じ要素が含まれているか確認する Archive](http://web.archive.org/web/20220223123108/https://www.javadrive.jp/python/list/index10.html)
[Sublime Text API Reference（翻訳） - Qiita Archive](http://web.archive.org/web/20220223123127/https://qiita.com/TalesofFox/items/387ef41d37a3c54a83eb)
[python - Get file syntax selection in sublime text 3 plugin - Stack Overflow Archive](http://web.archive.org/web/20220223123143/https://stackoverflow.com/questions/32740855/get-file-syntax-selection-in-sublime-text-3-plugin)
[Sublime Text 3でプラグインを作るメモ - Qiita Archive](http://web.archive.org/web/20220223123159/https://qiita.com/zaburo/items/2a579dfcc341c49452e9)
[API Reference – Sublime Text Documentation Archive](http://web.archive.org/web/20211218203021/https://www.sublimetext.com/docs/api_reference.html)
[Pythonのfor文でインデックスを同時に参照する：enumerate() | UX MILK Archive](http://web.archive.org/web/20220223123233/https://uxmilk.jp/8680)
[sublimetext2 - Sublime Text 3: How to install plugins from Github - Stack Overflow Archive](http://web.archive.org/web/20220309104742/https://stackoverflow.com/questions/23026201/sublime-text-3-how-to-install-plugins-from-github)