1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【VSCode Qiita Editor】記事の変更を監視しツリービューに反映

Last updated at Posted at 2025-08-16

前回の記事

概要

Zenn CLIには、 VS Code に統合する非公式の拡張 VS Code Zenn Editorがあり、Zenn の記事作成に非常に重宝している。

Qiita CLI にも同様の拡張があれば、便利そうなのだが、今のところ、該当する拡張機能を見つけることができない。

せめて、各記事をファイル名ではなく、title で表示するツリービューは欲しい。

そこで自分で拡張機能を作成してみるという試みです。

前回は、npx qiita publish を呼び出し、アクティブな記事を投稿するところまで実装しました。今回は、ファイルの変更を監視し、ツリービューの更新を行うところを実装します。

作成したソースはGitHub で公開しています。

ファイルの変更の監視

これまでファイルの新規作成と削除は監視していましたが、ファイルの変更を作成します。

            this.watcher.onDidChange((e) => {
                const filename = path.basename(e.fsPath);
                FrontMatterParser.parse(e).then((json) => {
                    if (filename.startsWith("new")) {
                        this.drafts.children.filter((value,index) => {
                            return value.path === e.path;
                        }).forEach((value,index) => {
                            value.name = json.title;
                            if (json.id) {
                                const newname = path.join(path.dirname(e.fsPath), json.id + ".md");
                                fs.renameSync(e.fsPath, newname);
                            }
                        });
                    } else {
                        this.published.children.filter((value,index) => {
                            return value.path === e.path;
                        }).forEach((value,index) => {
                            value.name = json.title;
                        });
                    }
                    this.refresh();
                });
            });

new で始まるファイルに FrontMatter で id が設定されている場合は、ファイル名を id に合わせ変更します。この際、ファイルの削除と新規作成が捕捉されるので既存の処理を修正する必要がありました。

ファイル削除時の処理を修正

これまで Drafts 以下のファイルが削除されることを想定していたので処理を修正します。

            this.watcher.onDidDelete((e) => {
                [this.published, this.drafts].forEach((parent, index) => {
                    parent.children.filter((value, index) => {
                        return value.path === e.path;
                    }).forEach(async (value, index) => {
                        parent.children.splice(index, 1);
                        const foundTab = vscode.window.tabGroups.all[0].tabs.filter(tab =>
                            (tab.input instanceof vscode.TabInputText) && (tab.input.uri.path === e.path)
                        );

                        if (foundTab.length === 1) {
                            await vscode.window.tabGroups.close(foundTab, false);
                        }
                    });
                });
                this.refresh();
            });

ファイル新規作成時の処理を修正

これまで npx qiita new で作成されたファイルのみを対象としていましたが、npx qiita publish 後にファイル名が修正された場合にも対応できるように修正します。

            this.watcher.onDidCreate(async (e) => {
                const filename = path.basename(e.fsPath);
                FrontMatterParser.parse(e).then(async (json) => {
                    let parent: QiitaTreeItem | undefined;
                    if (json.id) {
                        parent = this.published;
                    } else {
                        parent = this.drafts;
                    }
                    const article = new QiitaTreeItem(json.title, e.path);
                    parent.addChild(article);
                    if (parent === this.published) {
                        parent.children.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
                    } else {
                        parent.children.sort((a, b) => a.name.localeCompare(b.name));
                    }
                    this.refresh();
                    const doc = await vscode.workspace.openTextDocument(e.path);
                    await vscode.window.showTextDocument(doc, vscode.ViewColumn.One, true);
                });
            });

見通しが悪くなってきたのでそれぞれのイベントハンドラーをメソッドとして切り出す

    private watchFiles() {
        if (vscode.workspace && vscode.workspace.workspaceFolders) {
            this.watcher = vscode.workspace.createFileSystemWatcher(
                new vscode.RelativePattern(vscode.workspace.workspaceFolders[0], "public/*.md")
            );
            this.watcher.onDidCreate(uri => this.onDidCreateFile(uri) );
            this.watcher.onDidChange(uri => this.onDidChangeFile(uri) );
            this.watcher.onDidDelete(uri => this.onDidDeleteFile(uri) );
        }
    }

    private onDidDeleteFile(uri: vscode.Uri) {
        [this.published, this.drafts].forEach((parent, index) => {
            parent.children.filter((value, index) => {
                return value.path === uri.path;
            }).forEach(async (value, index) => {
                parent.children.splice(index, 1);
                const foundTab = vscode.window.tabGroups.all[0].tabs.filter(tab => (tab.input instanceof vscode.TabInputText) && (tab.input.uri.path === uri.path)
                );

                if (foundTab.length === 1) {
                    await vscode.window.tabGroups.close(foundTab, false);
                }
            });
        });
        this.refresh();
    }

    private onDidChangeFile(uri: vscode.Uri) {
        const filename = path.basename(uri.fsPath);
        FrontMatterParser.parse(uri).then((json) => {
            if (filename.startsWith("new")) {
                this.drafts.children.filter((value, index) => {
                    return value.path === uri.path;
                }).forEach((value, index) => {
                    value.name = json.title;
                    if (json.id) {
                        const newname = path.join(path.dirname(uri.fsPath), json.id + ".md");
                        fs.renameSync(uri.fsPath, newname);
                    }
                });
            } else {
                this.published.children.filter((value, index) => {
                    return value.path === uri.path;
                }).forEach((value, index) => {
                    value.name = json.title;
                });
            }
            this.refresh();
        });
    }

    private onDidCreateFile(uri: vscode.Uri) {
        const filename = path.basename(uri.fsPath);
        FrontMatterParser.parse(uri).then(async (json) => {
            let parent: QiitaTreeItem | undefined;
            if (json.id) {
                parent = this.published;
            } else {
                parent = this.drafts;
            }
            const article = new QiitaTreeItem(json.title, uri.path);
            parent.addChild(article);
            if (parent === this.published) {
                parent.children.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
            } else {
                parent.children.sort((a, b) => a.name.localeCompare(b.name));
            }
            this.refresh();
            const doc = await vscode.workspace.openTextDocument(uri.path);
            await vscode.window.showTextDocument(doc, vscode.ViewColumn.One, true);
        });
    }

想定されたノード以外が削除される場合がある

まず、QiitaTreeViewProvider 側で自前で splice で削除していたのを止め、QiitaTreeItemremoveChild メソッドを呼び出すように変更した。

    private async onDidDeleteFile(uri: vscode.Uri) {
        const foundTab = vscode.window.tabGroups.all[0].tabs.filter(tab => (tab.input instanceof vscode.TabInputText) && (tab.input.uri.path === uri.path)
        );

        if (foundTab.length === 1) {
            await vscode.window.tabGroups.close(foundTab, false);
        }
        [this.published, this.drafts].forEach((parent, index) => {
            parent.children.filter((value, index) => {
                return value.path === uri.path;
            }).forEach(async (value, index) => {
                parent.removeChild(value);
            });
        });
        this.refresh();
    }

ついでにエディタを閉じる処理をループ外に出した。

続いて、QiitaTreeItemremoveChild メソッドの処理を splice から filter に変更した。

    removeChild(child: QiitaTreeItem) {
        this._children = this.children.filter((value, index) => {
            return value !== child;
        });
    }

参考文献

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?