live preview mode working reliably now!

This commit is contained in:
Nathan Smith 2024-01-13 20:17:27 -05:00
parent 49e3e2eb97
commit 573f6ca802
4 changed files with 122 additions and 74 deletions

View file

@ -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;
}

View file

@ -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)) {

View file

@ -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
);
},
});
},
};

View file

@ -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);
}