mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
kind of working in live preview, just need to make sure we're not constantly re-rendering
This commit is contained in:
parent
697db638ea
commit
e21a45754a
11 changed files with 602 additions and 417 deletions
53
.eslintrc
53
.eslintrc
|
|
@ -1,23 +1,32 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": ["@typescript-eslint", "unused-imports"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-this-alias": "warn",
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
// replaced by 'unused-imports/no-unused-vars'
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
673
package-lock.json
generated
673
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -12,15 +12,19 @@
|
|||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"esbuild": "0.19.11",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4",
|
||||
"octokit": "^3.1.2"
|
||||
"octokit": "^3.1.2",
|
||||
"prettier": "^3.1.1",
|
||||
"tslib": "2.6.2",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/auth-oauth-device": "^6.0.1",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { App, Modal, setIcon } from "obsidian";
|
||||
import { Modal, setIcon } from "obsidian";
|
||||
|
||||
import { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import type { App } from "obsidian";
|
||||
import type { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
|
||||
export class AuthModal extends Modal {
|
||||
constructor(app: App, private readonly verification: Verification) {
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly verification: Verification,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
onOpen(): void {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { RequestUrlParam, requestUrl } from "obsidian";
|
||||
|
||||
import { OnVerificationCallback } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import type { OnVerificationCallback } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import type { RequestUrlParam } from "obsidian";
|
||||
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
||||
import { request } from "@octokit/request";
|
||||
import { requestUrl } from "obsidian";
|
||||
|
||||
const baseApi = "https://api.github.com";
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ export async function getPullRequest(org: string, repo: string, pr: number, toke
|
|||
{
|
||||
url: `${baseApi}/repos/${org}/${repo}/pulls/${pr}`,
|
||||
},
|
||||
token
|
||||
token,
|
||||
);
|
||||
return result.json as RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ export async function getCode(org: string, repo: string, path: string, branch: s
|
|||
{
|
||||
url: `${baseApi}/repos/${org}/${repo}/contents/${path}?ref=${branch}`,
|
||||
},
|
||||
token
|
||||
token,
|
||||
);
|
||||
return result.json as RestEndpointMethodTypes["repos"]["getContent"]["response"]["data"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import { MarkdownPostProcessorContext, setIcon } from "obsidian";
|
||||
import { getIssue, getPullRequest, parseUrl } from "./github";
|
||||
|
||||
import { PluginSettings } from "./plugin";
|
||||
|
||||
// import { PluginSettings } from "./plugin";
|
||||
|
||||
export async function InlineRenderer(el: HTMLElement, ctx: MarkdownPostProcessorContext) {
|
||||
const githubLinks = el.querySelectorAll<HTMLAnchorElement>(`a.external-link[href^="https://github.com"]`);
|
||||
for (const anchor of Array.from(githubLinks)) {
|
||||
const url = anchor.href;
|
||||
const parsedUrl = parseUrl(url);
|
||||
|
||||
let token: string | undefined;
|
||||
// Try and find the matching token
|
||||
if (parsedUrl.org) {
|
||||
let account = PluginSettings.accounts.find((acc) => acc.orgs.some((org) => org === parsedUrl.org));
|
||||
// Fall back to default token if available
|
||||
if (!account && PluginSettings.defaultAccount) {
|
||||
account = PluginSettings.accounts.find((acc) => acc.id === PluginSettings.defaultAccount);
|
||||
}
|
||||
token = account?.token;
|
||||
}
|
||||
|
||||
const container = createEl("a", { cls: "gh-link-inline", href: url });
|
||||
const icon = container.createSpan({ cls: "gh-link-inline-icon" });
|
||||
setIcon(icon, "github"); // TODO: Set correct icon
|
||||
if (parsedUrl.repo) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-repo",
|
||||
text: parsedUrl.repo,
|
||||
});
|
||||
} else if (parsedUrl.org) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-org",
|
||||
text: parsedUrl.org,
|
||||
});
|
||||
}
|
||||
if (parsedUrl.repo && parsedUrl.org) {
|
||||
if (parsedUrl.issue !== undefined) {
|
||||
const issue = await getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue, token);
|
||||
if (issue.title) {
|
||||
setIcon(icon, "square-dot");
|
||||
if (issue.pull_request?.merged_at) {
|
||||
icon.dataset.status = "done";
|
||||
} else {
|
||||
icon.dataset.status = issue.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-issue-title",
|
||||
text: issue.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (parsedUrl.pr !== undefined) {
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr, token);
|
||||
if (pull.title) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
if (pull.merged) {
|
||||
icon.dataset.status = "done";
|
||||
} else {
|
||||
icon.dataset.status = pull.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-pr-title",
|
||||
text: pull.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: add support for other stuff here
|
||||
}
|
||||
anchor.replaceWith(container);
|
||||
}
|
||||
}
|
||||
77
src/inline/inline.ts
Normal file
77
src/inline/inline.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { getIssue, getPullRequest, parseUrl } from "../github";
|
||||
|
||||
import { PluginSettings } from "../plugin";
|
||||
import { setIcon } from "obsidian";
|
||||
|
||||
// TODO: Split some of this out, there's no reason I should be getting the token when creating this element
|
||||
export async function createTag(href: string) {
|
||||
const parsedUrl = parseUrl(href);
|
||||
|
||||
let token: string | undefined;
|
||||
// Try and find the matching token
|
||||
if (parsedUrl.org) {
|
||||
let account = PluginSettings.accounts.find((acc) => acc.orgs.some((org) => org === parsedUrl.org));
|
||||
// Fall back to default token if available
|
||||
if (!account && PluginSettings.defaultAccount) {
|
||||
account = PluginSettings.accounts.find((acc) => acc.id === PluginSettings.defaultAccount);
|
||||
}
|
||||
token = account?.token;
|
||||
}
|
||||
|
||||
const container = createEl("a", { cls: "gh-link-inline", href });
|
||||
const icon = container.createSpan({ cls: "gh-link-inline-icon" });
|
||||
setIcon(icon, "github"); // TODO: Set correct icon
|
||||
if (parsedUrl.repo) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-repo",
|
||||
text: parsedUrl.repo,
|
||||
});
|
||||
} else if (parsedUrl.org) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-org",
|
||||
text: parsedUrl.org,
|
||||
});
|
||||
}
|
||||
if (parsedUrl.repo && parsedUrl.org) {
|
||||
if (parsedUrl.issue !== undefined) {
|
||||
const issue = await getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue, token);
|
||||
if (issue.title) {
|
||||
setIcon(icon, "square-dot");
|
||||
if (issue.pull_request?.merged_at) {
|
||||
icon.dataset.status = "done";
|
||||
} else {
|
||||
icon.dataset.status = issue.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-issue-title",
|
||||
text: issue.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (parsedUrl.pr !== undefined) {
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr, token);
|
||||
if (pull.title) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
if (pull.merged) {
|
||||
icon.dataset.status = "done";
|
||||
} else {
|
||||
icon.dataset.status = pull.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-pr-title",
|
||||
text: pull.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: add support for other stuff here
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
export async function InlineRenderer(el: HTMLElement) {
|
||||
const githubLinks = el.querySelectorAll<HTMLAnchorElement>(`a.external-link[href^="https://github.com"]`);
|
||||
for (const anchor of Array.from(githubLinks)) {
|
||||
const container = await createTag(anchor.href);
|
||||
anchor.replaceWith(container);
|
||||
}
|
||||
}
|
||||
72
src/inline/view-plugin.ts
Normal file
72
src/inline/view-plugin.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Component, editorLivePreviewField } from "obsidian";
|
||||
import { Decoration, MatchDecorator, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
import type { DecorationSet, EditorView, PluginSpec, PluginValue, ViewUpdate } from "@codemirror/view";
|
||||
|
||||
import type { GithubLinkPlugin } from "../plugin";
|
||||
import { createTag } from "./inline";
|
||||
import { valueWithin } from "src/util";
|
||||
|
||||
class InlineTagWidget extends WidgetType {
|
||||
private container: HTMLElement = createSpan();
|
||||
constructor(href: string) {
|
||||
super();
|
||||
createTag(href).then((tag) => {
|
||||
this.container.appendChild(tag);
|
||||
});
|
||||
}
|
||||
toDOM(): HTMLElement {
|
||||
return this.container;
|
||||
}
|
||||
}
|
||||
|
||||
export function createInlineViewPlugin(plugin: GithubLinkPlugin) {
|
||||
class InlineViewPluginValue implements PluginValue {
|
||||
private readonly component = new Component();
|
||||
private readonly plugin: GithubLinkPlugin;
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
constructor(view: EditorView) {
|
||||
this.plugin = plugin;
|
||||
this.component.load();
|
||||
this.updateDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate): void {
|
||||
this.updateDecorations(update.view);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.component.unload();
|
||||
this.decorations = Decoration.none;
|
||||
}
|
||||
|
||||
updateDecorations(view: EditorView) {
|
||||
this.decorations = new MatchDecorator({
|
||||
regexp: /(https:\/\/)?github\.com[\S]+/g,
|
||||
decoration: (match, view, pos) => {
|
||||
const shouldRender = this.shouldRender(view, pos);
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
return Decoration.replace({ widget: new InlineTagWidget(match[0]) });
|
||||
},
|
||||
}).createDeco(view);
|
||||
}
|
||||
|
||||
isLivePreview(state: EditorView["state"]): boolean {
|
||||
return state.field(editorLivePreviewField);
|
||||
}
|
||||
|
||||
shouldRender(view: EditorView, pos: number) {
|
||||
const selection = view.state.selection;
|
||||
const isLivePreview = this.isLivePreview(view.state);
|
||||
const cursorOverlap = selection.ranges.some((range) => valueWithin(pos, range.from, range.to));
|
||||
return isLivePreview && !cursorOverlap;
|
||||
}
|
||||
}
|
||||
|
||||
const InlineViewPluginSpec: PluginSpec<InlineViewPluginValue> = {
|
||||
decorations: (plugin) => plugin.decorations,
|
||||
};
|
||||
|
||||
return ViewPlugin.fromClass(InlineViewPluginValue, InlineViewPluginSpec);
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { GithubLinkPluginSettings, GithubLinkPluginSettingsTab } from "./settings";
|
||||
|
||||
import { InlineRenderer } from "./inline";
|
||||
import type { GithubLinkPluginSettings } from "./settings";
|
||||
import { GithubLinkPluginSettingsTab } from "./settings";
|
||||
import { InlineRenderer } from "./inline/inline";
|
||||
import { Plugin } from "obsidian";
|
||||
import { createInlineViewPlugin } from "./inline/view-plugin";
|
||||
import { getIssue } from "./github";
|
||||
|
||||
export let PluginSettings: GithubLinkPluginSettings = { accounts: [] };
|
||||
|
|
@ -11,6 +12,7 @@ export class GithubLinkPlugin extends Plugin {
|
|||
PluginSettings = Object.assign({}, await this.loadData());
|
||||
this.addSettingTab(new GithubLinkPluginSettingsTab(this.app, this));
|
||||
this.registerMarkdownPostProcessor(InlineRenderer);
|
||||
this.registerEditorExtension(createInlineViewPlugin(this));
|
||||
this.addCommand({
|
||||
id: "get-issue",
|
||||
name: "Get GitHub issue",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import { GithubLinkPlugin, PluginSettings } from "./plugin";
|
||||
import { PluginSettingTab, Setting } from "obsidian";
|
||||
|
||||
import type { App } from "obsidian";
|
||||
import { AuthModal } from "./auth-modal";
|
||||
import { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import type { GithubLinkPlugin } from "./plugin";
|
||||
import { PluginSettings } from "./plugin";
|
||||
import type { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import { auth } from "./github";
|
||||
|
||||
export interface GithubAccount {
|
||||
|
|
@ -22,7 +24,10 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
authModal: AuthModal | null = null;
|
||||
newAccount: GithubAccount | null = null;
|
||||
|
||||
constructor(public app: App, private readonly plugin: GithubLinkPlugin) {
|
||||
constructor(
|
||||
public app: App,
|
||||
private readonly plugin: GithubLinkPlugin,
|
||||
) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +94,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
new Setting(accountContainer)
|
||||
.setName("Orgs and users")
|
||||
.setDesc(
|
||||
"A comma separated list of the GitHub organizations and users this account should be used for. Optional."
|
||||
"A comma separated list of the GitHub organizations and users this account should be used for. Optional.",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setValue(this.newAccount!.orgs.join(", "));
|
||||
|
|
@ -100,7 +105,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
new Setting(accountContainer)
|
||||
.setName("Token")
|
||||
.setDesc(
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens). Required."
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens). Required.",
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Generate Token");
|
||||
|
|
@ -160,7 +165,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
new Setting(accountContainer)
|
||||
.setName("Orgs and users")
|
||||
.setDesc(
|
||||
"A comma separated list of the GitHub organizations and users this account should be used for."
|
||||
"A comma separated list of the GitHub organizations and users this account should be used for.",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setValue(account.orgs.join(", "));
|
||||
|
|
@ -172,7 +177,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
new Setting(accountContainer)
|
||||
.setName("Token")
|
||||
.setDesc(
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens)."
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens).",
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Generate Token");
|
||||
|
|
|
|||
3
src/util.ts
Normal file
3
src/util.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function valueWithin(value: number, min: number, max: number) {
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
Loading…
Reference in a new issue