mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
live preview mode working reliably now!
This commit is contained in:
parent
49e3e2eb97
commit
573f6ca802
4 changed files with 122 additions and 74 deletions
|
|
@ -2,32 +2,35 @@ import type { IssueResponse, PullResponse } from "./response";
|
|||
|
||||
import { Cache } from "./cache";
|
||||
import { api } from "./api";
|
||||
import { PluginSettings } from "src/plugin";
|
||||
|
||||
const cache = new Cache();
|
||||
|
||||
export async function getIssue(org: string, repo: string, issue: number, token?: string): Promise<IssueResponse> {
|
||||
function getToken(org: string): string | undefined {
|
||||
const account =
|
||||
PluginSettings.accounts.find((acc) => acc.orgs.some((savedOrg) => savedOrg === org)) ??
|
||||
PluginSettings.accounts.find((acc) => acc.id === PluginSettings.defaultAccount);
|
||||
return account?.token;
|
||||
}
|
||||
|
||||
export async function getIssue(org: string, repo: string, issue: number): Promise<IssueResponse> {
|
||||
const cachedValue = cache.getIssue(org, repo, issue);
|
||||
if (cachedValue) {
|
||||
return Promise.resolve(cachedValue);
|
||||
}
|
||||
|
||||
const response = await api.getIssue(org, repo, issue, token);
|
||||
const response = await api.getIssue(org, repo, issue, getToken(org));
|
||||
cache.setIssue(org, repo, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getPullRequest(
|
||||
org: string,
|
||||
repo: string,
|
||||
pullRequest: number,
|
||||
token?: string,
|
||||
): Promise<PullResponse> {
|
||||
export async function getPullRequest(org: string, repo: string, pullRequest: number): Promise<PullResponse> {
|
||||
const cachedValue = cache.getPullRequest(org, repo, pullRequest);
|
||||
if (cachedValue) {
|
||||
return Promise.resolve(cachedValue);
|
||||
}
|
||||
|
||||
const response = await api.getPullRequest(org, repo, pullRequest, token);
|
||||
const response = await api.getPullRequest(org, repo, pullRequest, getToken(org));
|
||||
cache.setPullRequest(org, repo, response);
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,36 @@
|
|||
import { getIssue, getPullRequest } from "../github/github";
|
||||
|
||||
import { PluginSettings } from "../plugin";
|
||||
import { parseUrl } from "../github/url-parse";
|
||||
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);
|
||||
const container = createEl("a", { cls: "gh-link-inline-tag", 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;
|
||||
}
|
||||
// Add icon
|
||||
const icon = createTagSection(container).createSpan({ cls: "gh-link-inline-tag-icon" });
|
||||
setIcon(icon, "github");
|
||||
|
||||
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
|
||||
// Add repo
|
||||
if (parsedUrl.repo) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-repo",
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-repo",
|
||||
text: parsedUrl.repo,
|
||||
});
|
||||
} else if (parsedUrl.org) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-org",
|
||||
}
|
||||
|
||||
// fall back to org if no repo found
|
||||
else if (parsedUrl.org) {
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-org",
|
||||
text: parsedUrl.org,
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedUrl.repo && parsedUrl.org) {
|
||||
// Get issue info
|
||||
if (parsedUrl.issue !== undefined) {
|
||||
const issue = await getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue, token);
|
||||
const issue = await getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue);
|
||||
if (issue.title) {
|
||||
setIcon(icon, "square-dot");
|
||||
if (issue.pull_request?.merged_at) {
|
||||
|
|
@ -43,14 +38,16 @@ export async function createTag(href: string) {
|
|||
} else {
|
||||
icon.dataset.status = issue.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-issue-title",
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-issue-title",
|
||||
text: issue.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get PR info
|
||||
if (parsedUrl.pr !== undefined) {
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr, token);
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr);
|
||||
if (pull.title) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
if (pull.merged) {
|
||||
|
|
@ -58,8 +55,8 @@ export async function createTag(href: string) {
|
|||
} else {
|
||||
icon.dataset.status = pull.state;
|
||||
}
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-pr-title",
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-pr-title",
|
||||
text: pull.title,
|
||||
});
|
||||
}
|
||||
|
|
@ -69,6 +66,10 @@ export async function createTag(href: string) {
|
|||
return container;
|
||||
}
|
||||
|
||||
function createTagSection(parent: HTMLElement): HTMLElement {
|
||||
return parent.createDiv({ cls: "gh-link-inline-tag-section" });
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,24 @@ import type { DecorationSet, EditorView, PluginSpec, PluginValue, ViewUpdate } f
|
|||
import type { GithubLinkPlugin } from "../plugin";
|
||||
import { createTag } from "./inline";
|
||||
|
||||
interface DecoSpec {
|
||||
widget?: InlineTagWidget;
|
||||
}
|
||||
|
||||
class InlineTagWidget extends WidgetType {
|
||||
public error = false;
|
||||
private container: HTMLElement = createSpan();
|
||||
constructor(href: string) {
|
||||
constructor(href: string, dispatch: () => void) {
|
||||
super();
|
||||
createTag(href).then((tag) => {
|
||||
this.container.appendChild(tag);
|
||||
});
|
||||
createTag(href)
|
||||
.then((tag) => {
|
||||
this.container.appendChild(tag);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.error = true;
|
||||
dispatch(); // Force an update of decorations
|
||||
});
|
||||
}
|
||||
toDOM(): HTMLElement {
|
||||
return this.container;
|
||||
|
|
@ -25,12 +36,17 @@ export function createInlineViewPlugin(plugin: GithubLinkPlugin) {
|
|||
private readonly plugin: GithubLinkPlugin;
|
||||
private readonly match = new MatchDecorator({
|
||||
regexp: /(https:\/\/)?github\.com[\S]+/g,
|
||||
decoration: (match, view) => {
|
||||
const shouldRender = this.shouldRender(view);
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
decorate: (add, from, to, match, view) => {
|
||||
const shouldRender = this.shouldRender(view, from, to);
|
||||
if (shouldRender) {
|
||||
add(
|
||||
from,
|
||||
to,
|
||||
Decoration.replace({
|
||||
widget: new InlineTagWidget(match[0], view.dispatch),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Decoration.replace({ widget: new InlineTagWidget(match[0]) });
|
||||
},
|
||||
});
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
|
|
@ -62,8 +78,15 @@ export function createInlineViewPlugin(plugin: GithubLinkPlugin) {
|
|||
return state.field(editorLivePreviewField);
|
||||
}
|
||||
|
||||
shouldRender(view: EditorView) {
|
||||
return this.isLivePreview(view.state);
|
||||
shouldRender(view: EditorView, decorationFrom: number, decorationTo: number) {
|
||||
const overlap = view.state.selection.ranges.some((r) => {
|
||||
if (r.from <= decorationFrom) {
|
||||
return r.to >= decorationFrom;
|
||||
} else {
|
||||
return r.from <= decorationTo;
|
||||
}
|
||||
});
|
||||
return !overlap && this.isLivePreview(view.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,22 +95,29 @@ export function createInlineViewPlugin(plugin: GithubLinkPlugin) {
|
|||
// Update and return decorations for the CodeMirror view
|
||||
|
||||
return plugin.decorations.update({
|
||||
filter: (rangeFrom, rangeTo) =>
|
||||
filter: (rangeFrom, rangeTo, deco) => {
|
||||
const widget = (deco.spec as DecoSpec).widget;
|
||||
if (widget && widget.error) {
|
||||
return false;
|
||||
}
|
||||
// Check if the range is collapsed (cursor position)
|
||||
rangeFrom === rangeTo ||
|
||||
// Check if there are no overlapping selection ranges
|
||||
!plugin.view.state.selection.ranges.filter((selectionRange) => {
|
||||
// Determine the start and end positions of the selection range
|
||||
const selectionStart = selectionRange.from;
|
||||
const selectionEnd = selectionRange.to;
|
||||
return (
|
||||
rangeFrom === rangeTo ||
|
||||
// Check if there are no overlapping selection ranges
|
||||
!plugin.view.state.selection.ranges.filter((selectionRange) => {
|
||||
// Determine the start and end positions of the selection range
|
||||
const selectionStart = selectionRange.from;
|
||||
const selectionEnd = selectionRange.to;
|
||||
|
||||
// Check if the selection range overlaps with the specified range
|
||||
if (selectionStart < rangeFrom) {
|
||||
return selectionEnd > rangeFrom; // Overlapping condition
|
||||
} else {
|
||||
return selectionStart < rangeTo; // Overlapping condition
|
||||
}
|
||||
}).length,
|
||||
// Check if the selection range overlaps with the specified range
|
||||
if (selectionStart <= rangeFrom) {
|
||||
return selectionEnd >= rangeFrom; // Overlapping condition
|
||||
} else {
|
||||
return selectionStart <= rangeTo; // Overlapping condition
|
||||
}
|
||||
}).length
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
42
styles.css
42
styles.css
|
|
@ -146,9 +146,8 @@ body.theme-dark {
|
|||
font-size: 3em;
|
||||
}
|
||||
|
||||
.gh-link-inline {
|
||||
.gh-link-inline-tag {
|
||||
display: inline-flex;
|
||||
line-height: 1em;
|
||||
background-color: var(--gh-color-canvas-default);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--gh-color-border-default);
|
||||
|
|
@ -158,47 +157,62 @@ body.theme-dark {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.gh-link-inline:hover {
|
||||
.gh-link-inline-tag:hover {
|
||||
color: var(--gh-color-fg-default);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gh-link-inline-icon {
|
||||
background-color: var(--gh-color-canvas-inset);
|
||||
display: block;
|
||||
.gh-link-inline-tag-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-icon {
|
||||
display: inline-flex;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.gh-link-inline-icon[data-status="open"] {
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon) {
|
||||
background-color: var(--gh-color-canvas-inset);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="open"]) {
|
||||
background-color: var(--gh-color-open-subtle);
|
||||
color: var(--gh-color-open-fg);
|
||||
border-color: var(--gh-color-open-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-icon[data-status="closed"] {
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="closed"]) {
|
||||
background-color: var(--gh-color-closed-subtle);
|
||||
color: var(--gh-color-closed-fg);
|
||||
border-color: var(--gh-color-closed-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-icon[data-status="done"] {
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="done"]) {
|
||||
background-color: var(--gh-color-done-subtle);
|
||||
color: var(--gh-color-done-fg);
|
||||
border-color: var(--gh-color-done-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-repo {
|
||||
padding: 0 6px;
|
||||
line-height: var(--line-height-normal);
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-repo) {
|
||||
background-color: var(--gh-color-accent-subtle);
|
||||
color: var(--gh-color-accent-fg);
|
||||
border-color: var(--gh-color-accent-emphasis);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gh-link-inline-pr-title {
|
||||
.gh-link-inline-tag-repo {
|
||||
text-wrap: nowrap;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-pr-title,
|
||||
.gh-link-inline-tag-issue-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
padding: 0 4px;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue