Skip to content
Merged

Dev #460

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// URL namespace under which the server serves local images.
// The browser rewrites image srcs to this prefix (via pantsdown config)
// and the server maps it back to filesystem paths relative to root.
export const IMAGE_PREFIX = "/__github_preview__/image/";
10 changes: 9 additions & 1 deletion app/github-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export class GithubPreview {
logging: { level: ENV.LOG_LEVEL === "none" ? undefined : ENV.LOG_LEVEL },
});

nvim.onDisconnect(() => {
// neovim is gone, don't linger as an orphaned server process
process.exit(0);
});

const props = (await nvim.call("nvim_get_var", ["github_preview_props"])) as PluginProps;
if (ENV.IS_DEV) PluginPropsSchema.parse(props);

Expand Down Expand Up @@ -107,6 +112,9 @@ export class GithubPreview {
}

async getEntries(path: string): Promise<string[]> {
// do not return any entries outside of repo root
if (!normalize(this.root + path).startsWith(this.root)) return [];

const currentDir = path.endsWith("/") ? path : dirname(path) + "/";
const paths = await globby(currentDir + "*", {
cwd: this.root,
Expand Down Expand Up @@ -134,7 +142,7 @@ export class GithubPreview {
async setCurrPath(path: string): Promise<undefined | string[]> {
// do not return any entries outside of repo root
const normalized = normalize(this.root + path);
if (normalized.length < this.root.length) return;
if (!normalized.startsWith(this.root)) return;

this.currentPath = path;

Expand Down
49 changes: 34 additions & 15 deletions app/nvim/on-content-change.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { relative } from "node:path";
import { type GithubPreview } from "../github-preview.ts";

const NOTIFICATION = "attach_buffer";
Expand All @@ -15,19 +16,28 @@ export async function onContentChange(
if (attachedBuffer === buffer) attachedBuffer = null;
});

// Notification handler
app.nvim.onNotification(NOTIFICATION, async ([buffer, path]) => {
// Notification handler.
// Handled through a queue: rapid buffer switches could otherwise interleave
// across the awaits below and detach/attach the wrong buffer
let attachQueue = Promise.resolve();
app.nvim.onNotification(NOTIFICATION, ([buffer, path]) => {
if (!path) return;

if (attachedBuffer !== buffer) {
if (attachedBuffer !== null) {
await app.nvim.call("nvim_buf_detach", [attachedBuffer]);
attachedBuffer = null;
}
// attach to buffer to receive content change notifications
const attached = await app.nvim.call("nvim_buf_attach", [buffer, true, {}]);
if (attached) attachedBuffer = buffer;
}
attachQueue = attachQueue
.then(async () => {
if (attachedBuffer === buffer) return;

if (attachedBuffer !== null) {
await app.nvim.call("nvim_buf_detach", [attachedBuffer]);
attachedBuffer = null;
}
// attach to buffer to receive content change notifications
const attached = await app.nvim.call("nvim_buf_attach", [buffer, true, {}]);
if (attached) attachedBuffer = buffer;
})
.catch((err: unknown) => {
app.nvim.logger?.error({ attach_buffer_error: err });
});
});

// Create autocmd to notify us with event "attach_buffer"
Expand All @@ -53,10 +63,19 @@ export async function onContentChange(
async ([buffer, _changedtick, firstline, lastline, linedata, _more]) => {
const path = await app.nvim.call("nvim_buf_get_name", [buffer]);
const replaceAll = lastline === -1 && firstline === 0;
const deleteCount = lastline - firstline;
const newContent = replaceAll
? linedata
: app.lines.toSpliced(firstline, deleteCount, ...linedata);

let newContent: string[];
if (replaceAll) {
newContent = linedata;
} else if (relative(app.root, path) === app.currentPath) {
const deleteCount = lastline - firstline;
newContent = app.lines.toSpliced(firstline, deleteCount, ...linedata);
} else {
// app.lines mirrors a different file (e.g. browser navigated away),
// an incremental splice would corrupt it. Fetch the full buffer instead.
newContent = await app.nvim.call("nvim_buf_get_lines", [buffer, 0, -1, true]);
}

callback(newContent, path);
},
);
Expand Down
20 changes: 16 additions & 4 deletions app/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { normalize } from "node:path";
import { type Server } from "bun";
import opener from "opener";
import { IMAGE_PREFIX } from "../consts.ts";
import { type GithubPreview } from "../github-preview.ts";
import index from "../web/index.html";
import { websocketHandler } from "./websocket.ts";
Expand All @@ -12,13 +14,23 @@ export function startServer<T>(app: GithubPreview, isDev: boolean): Server<T> {
const server = Bun.serve({
port: port,
routes: {
"/__github_preview__/image/*": (req) => {
[IMAGE_PREFIX + "*"]: (req: Request) => {
app.nvim.logger?.info({ route: req.url });
const pathname = new URL(req.url).pathname;
const filePath = pathname.replace("/__github_preview__/image/", "");
app.nvim.logger?.info({ filePath: app.root + filePath });
let filePath: string;
try {
filePath = decodeURIComponent(pathname.replace(IMAGE_PREFIX, ""));
} catch (_err) {
return new Response(null, { status: 400 });
}
// do not serve any files outside of repo root
const fullPath = normalize(app.root + filePath);
if (!fullPath.startsWith(app.root)) {
return new Response(null, { status: 404 });
}
app.nvim.logger?.info({ filePath: fullPath });
// images with relative sources
const file = Bun.file(app.root + filePath);
const file = Bun.file(fullPath);
return new Response(file);
},
[UNALIVE_URL]: async (req) => {
Expand Down
3 changes: 1 addition & 2 deletions app/web/components/markdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Pantsdown } from "pantsdown";
import { useContext, useEffect, useState } from "react";
import { IMAGE_PREFIX } from "../../../consts.ts";
import { cn, getFileExt } from "../../utils.ts";
import { websocketContext } from "../websocket-provider/context.ts";
import { BreadCrumbs } from "./breadcrumbs.tsx";
Expand All @@ -13,8 +14,6 @@ import { getScrollOffsets, type Offsets } from "./scroll.ts";
const MARKDOWN_CONTAINER_ID = "markdown-container-id";
const MARKDOWN_ELEMENT_ID = "markdown-element-id";

const IMAGE_PREFIX = "/__github_preview__/image/";

const pantsdown = new Pantsdown({
renderer: {
relativeImageUrlPrefix: IMAGE_PREFIX,
Expand Down
10 changes: 5 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions lua/github-preview/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ M.start = function()
return
end

-- should look like "/Users/.../github-preview"
local root = Config.value.single_file and "" or vim.fn.finddir(".git", ";")
-- single-file mode may also be enabled as a fallback when no repo is found.
-- keep it local so the fallback doesn't stick to Config.value across starts
local single_file = Config.value.single_file

local buffer_name = vim.api.nvim_buf_get_name(0)
local init_path = vim.fn.fnamemodify(buffer_name, ":p")

-- search for ".git" upwards starting from the current file's directory,
-- not from cwd: the file being edited may live outside of nvim's cwd.
-- should look like "/Users/.../github-preview/.git"
local root = single_file and "" or vim.fn.finddir(".git", vim.fn.fnamemodify(init_path, ":h") .. ";")

if root == "" then
-- if repo root not found or single-file mode is enabled,
-- we make sure there's something loaded into the current buffer
Expand All @@ -58,7 +64,7 @@ M.start = function()

-- if no root, we set root to current path
root = vim.fn.fnamemodify(init_path, ":h") .. "/"
Config.value.single_file = true
single_file = true
else
-- if found, path is made absolute & has "/.git/" popped
root = vim.fn.fnamemodify(root, ":p:h:h") .. "/"
Expand All @@ -70,7 +76,7 @@ M.start = function()
root = root,
path = init_path,
},
config = Config.value,
config = vim.tbl_deep_extend("force", Config.value, { single_file = single_file }),
}
-- vim.g.github_preview_props is read by bunvim
vim.g.github_preview_props = github_preview_props
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
},
"dependencies": {
"bun-plugin-tailwind": "^0.1.2",
"bunvim": "1.2.0",
"bunvim": "1.3.0",
"clsx": "^2.1.1",
"globby": "^16.2.2",
"isbinaryfile": "6.0.0",
"mermaid": "^11.16.0",
"opener": "^1.5.2",
"pantsdown": "2.2.6",
"pantsdown": "2.2.7",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"reconnecting-websocket": "^4.4.0",
Expand Down