Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
lambdalisue committed Jun 27, 2023
1 parent 11f640c commit 17a2ee9
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
101 changes: 101 additions & 0 deletions denops/gin/command/browse/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Denops } from "https://deno.land/x/denops_std@v5.0.1/mod.ts";
import { unnullish } from "https://deno.land/x/unnullish@v1.0.1/mod.ts";
import * as path from "https://deno.land/std@0.192.0/path/mod.ts";
import * as option from "https://deno.land/x/denops_std@v5.0.1/option/mod.ts";
import { systemopen } from "https://deno.land/x/systemopen@v0.2.0/mod.ts";
import { findWorktreeFromDenops } from "../../git/worktree.ts";
import { getRemoteURL } from "../../git/remote.ts";
import { execute } from "../../git/executor.ts";
import { getHostingService } from "../../git/hosting_service.ts";
import { yank } from "../../util/yank.ts";
import { decodeUtf8 } from "../../util/text.ts";

export type ExecOptions = {
worktree?: string;
commitish?: string;
filename?: string;
remote?: string;
permanent?: boolean;
yank?: boolean;
echo?: boolean;
};

export async function exec(
denops: Denops,
options: ExecOptions = {},
): Promise<void> {
const url = await getURL(denops, options);
if (options.yank) {
await yank(denops, url.href);
}
if (options.echo) {
await denops.cmd(`echomsg url`, { url: url.href });
} else {
await systemopen(url.href);
}
}

async function getURL(
denops: Denops,
options: ExecOptions,
) {
const verbose = await option.verbose.get(denops);
const worktree = await findWorktreeFromDenops(denops, {
worktree: options.worktree,
verbose: !!verbose,
});

const remoteURL = await getRemoteURL(denops, options.remote ?? "origin", {
worktree: options.worktree,
});
const svc = await getHostingService(remoteURL);
if (options.filename) {
const [filename, lineStart, lineEnd] = parseFilename(
worktree,
options.filename,
);
const commitish = options.permanent
? await getCommitishHash(
denops,
worktree,
options.commitish ?? "HEAD",
)
: options.commitish ?? "HEAD";
return svc.getBlobURL(commitish, filename, { lineStart, lineEnd });
} else {
return svc.getRootURL();
}
}

function parseFilename(
worktree: string,
filename: string,
): [string, number?, number?] {
const relpath = path.isAbsolute(filename)
? path.relative(worktree, filename)
: filename;
const m = relpath?.match(/^(.*?):(\d+)(?::(\d+))?$/);
if (!m) {
return [relpath, undefined, undefined];
}
return [m[1], unnullish(m[2], Number), unnullish(m[3], Number)];
}

async function getCommitishHash(
denops: Denops,
worktree: string,
commitish: string | typeof HEAD,
): Promise<string> {
const { success, stdout, stderr } = await execute(denops, [
"rev-parse",
commitish.toString(),
], {
worktree: worktree,
stdoutIndicator: "null",
stderrIndicator: "null",
});
if (!success) {
throw new Error(decodeUtf8(stderr));
}
return decodeUtf8(stdout).trim();
}
67 changes: 67 additions & 0 deletions denops/gin/command/browse/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Denops } from "https://deno.land/x/denops_std@v5.0.1/mod.ts";
import { assert, is } from "https://deno.land/x/unknownutil@v3.2.0/mod.ts#^";
import * as helper from "https://deno.land/x/denops_std@v5.0.1/helper/mod.ts";
import {
parseOpts,
validateOpts,
} from "https://deno.land/x/denops_std@v5.0.1/argument/opts.ts";
import { normCmdArgs, parseSilent } from "../../util/cmd.ts";
import { exec } from "./command.ts";

export function main(denops: Denops): void {
denops.dispatcher = {
...denops.dispatcher,
"browse:command": (mods, args, range) => {
assert(mods, is.String, { message: "mods must be string" });
assert(args, is.ArrayOf(is.String), { message: "args must be string[]" });
assert(range, is.TupleOf([is.Number, is.Number] as const), {
message: "range must be [number, number]",
});
const silent = parseSilent(mods);
return helper.ensureSilent(denops, silent, () => {
return helper.friendlyCall(denops, () => command(denops, args, range));
});
},
};
}

async function command(
denops: Denops,
args: string[],
_range: readonly [number, number],
): Promise<void> {
const [opts, residue] = parseOpts(await normCmdArgs(denops, args));
validateOpts(opts, [
"worktree",
"remote",
"permanent",
"yank",
"echo",
]);
const [commitish, filename] = parseResidue(residue);
await exec(denops, {
worktree: opts.worktree,
remote: opts.remote,
commitish,
filename,
permanent: "permanent" in opts,
yank: "yank" in opts,
echo: "echo" in opts,
});
}

function parseResidue(residue: string[]): [string?, string?] {
// GinBrowse [{options}]
// GinBrowse [{options}] {path}
// GinBrowse [{options}] {commitish} {path}
switch (residue.length) {
case 0:
return [undefined, undefined];
case 1:
return [undefined, residue[0]];
case 2:
return [residue[0], residue[1]];
default:
throw new Error("Invalid number of arguments");
}
}
2 changes: 2 additions & 0 deletions denops/gin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { main as mainProxy } from "./proxy/main.ts";
import { main as mainUtil } from "./util/main.ts";

import { main as mainBranch } from "./command/branch/main.ts";
import { main as mainBrowse } from "./command/browse/main.ts";
import { main as mainChaperon } from "./command/chaperon/main.ts";
import { main as mainDiff } from "./command/diff/main.ts";
import { main as mainEdit } from "./command/edit/main.ts";
Expand All @@ -24,6 +25,7 @@ export function main(denops: Denops): void {
mainUtil(denops);

mainBranch(denops);
mainBrowse(denops);
mainChaperon(denops);
mainDiff(denops);
mainEdit(denops);
Expand Down
31 changes: 31 additions & 0 deletions doc/gin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,37 @@ COMMANDS *gin-commands*

Users can specify default arguments by |g:gin_branch_default_args|.
Use a bang (!) to forcibly open a buffer.
Use an ampersand (&) to temporary disable default arguments.

*:GinBrowse*
:GinBrowse[&] [{++option}...]
:GinBrowse[&] [{++option}...] [{commitish}] {path}[:{lineStart}[:{lineEnd}]
:[range]GinBrowse[&] [{++option}...] [{commitish}]
Open a system default browser to visit a web page of a hosting service
of the {worktree} (current) repository (e.g. GitHub).
If no {path} or {range} is specified, it opens the index page of the
hosting service.
If {path} is specified, it opens the page of the {path} in the hosting
service with specified {commitish} (default is "HEAD").
If {range} is specified, it opens the page of the current buffer in
the hosting service with specified {commitish} (default is "HEAD").

The following options are valid as {++option}:

++remote={remote}
Use {remote} as a remote to generate a URL.
Default is "origin".

++permanent
Use an exact revision to generate a permanent URL.

++yank
Copy the URL to the clipboard.

++echo
Echo URL instead of opening a system browser.

Users can specify default arguments by |g:gin_browse_default_args|.
Use an ampersand (&) to temporary disable default arguments.

*:GinCd*
Expand Down
13 changes: 13 additions & 0 deletions plugin/gin-browse.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
if exists('g:loaded_gin_browse')
finish
endif
let g:loaded_gin_browse = 1

function! s:command(mods, args, range) abort
if denops#plugin#wait('gin')
return
endif
call denops#request('gin', 'browse:command', [a:mods, a:args, a:range])
endfunction

command! -bar -range=% -nargs=* GinBrowse call s:command(<q-mods>, [<f-args>], [<line1>, <line2>])

0 comments on commit 17a2ee9

Please sign in to comment.