前回の記事
概要
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
で削除していたのを止め、QiitaTreeItem
の removeChild
メソッドを呼び出すように変更した。
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();
}
ついでにエディタを閉じる処理をループ外に出した。
続いて、QiitaTreeItem
の removeChild
メソッドの処理を splice
から filter
に変更した。
removeChild(child: QiitaTreeItem) {
this._children = this.children.filter((value, index) => {
return value !== child;
});
}