Skip to content

Commit

Permalink
Merge pull request #92 from lambdalisue/gin-browse
Browse files Browse the repository at this point in the history
👍 Add `GinBrowse` command
  • Loading branch information
lambdalisue authored Aug 5, 2023
2 parents 45e9fce + e3896a0 commit 63904d7
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ without announcements.**
- `Gin` to call a raw git command and echo the result
- `GinBuffer` to call a raw git command and open a result buffer
- `GinBranch` to see `git branch` of a repository
- `GinBrowse` to visit the hosting service webpage of a repository (powered by
[git-browse](https://deno.land/x/git_browse))
- `GinCd/GinLcd/GinTcd` to invoke `cd/lcd/tcd` to the repository root
- `GinChaperon` to solve git conflicts (like `git mergetool`)
- `GinDiff` to see `git diff` of a file
Expand Down
67 changes: 67 additions & 0 deletions denops/gin/command/browse/command.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 { unnullish } from "https://deno.land/x/unnullish@v1.0.1/mod.ts";
import { assert, is } from "https://deno.land/x/unknownutil@v3.4.0/mod.ts";
import { systemopen } from "https://deno.land/x/systemopen@v0.2.0/mod.ts";
import {
getURL,
Options,
} from "https://deno.land/x/git_browse@v0.3.0/bin/browse.ts";
import * as batch from "https://deno.land/x/denops_std@v5.0.1/batch/mod.ts";
import * as vars from "https://deno.land/x/denops_std@v5.0.1/variable/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 { findWorktreeFromDenops } from "../../git/worktree.ts";
import { yank } from "../../util/yank.ts";

export type ExecOptions = Omit<Options, "cwd" | "aliases"> & {
worktree?: string;
yank?: boolean;
noBrowser?: boolean;
};

export async function exec(
denops: Denops,
commitish: string,
options: ExecOptions = {},
): Promise<void> {
const [verbose, aliases] = await batch.collect(denops, (denops) => [
option.verbose.get(denops),
vars.g.get(denops, "git_browse_aliases", {}),
]);
assert(aliases, is.RecordOf(is.String), {
message: "g:git_browse_aliases must be a string dictionary",
});

const worktree = await findWorktreeFromDenops(denops, {
worktree: options.worktree,
verbose: !!verbose,
});

options.path = unnullish(
options.path,
(p) => path.isAbsolute(p) ? path.relative(worktree, p) : p,
);
options.path = unnullish(
options.path,
(p) => p === "" ? "." : p,
);
const url = await getURL(commitish, {
cwd: worktree,
remote: options.remote,
path: options.path,
home: options.home,
commit: options.commit,
pr: options.pr,
permalink: options.permalink,
aliases,
});

if (options.yank) {
await yank(denops, url.href);
}
if (options.noBrowser) {
await denops.cmd("echomsg url", { url: url.href });
} else {
await systemopen(url.href);
}
}
141 changes: 141 additions & 0 deletions denops/gin/command/browse/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 {
assert,
ensure,
is,
} from "https://deno.land/x/unknownutil@v3.0.0/mod.ts#^";
import * as batch from "https://deno.land/x/denops_std@v5.0.1/batch/mod.ts";
import * as fn from "https://deno.land/x/denops_std@v5.0.1/function/mod.ts";
import * as vars from "https://deno.land/x/denops_std@v5.0.1/variable/mod.ts";
import * as helper from "https://deno.land/x/denops_std@v5.0.1/helper/mod.ts";
import {
parse,
validateFlags,
validateOpts,
} from "https://deno.land/x/denops_std@v5.0.1/argument/mod.ts";
import { normCmdArgs, parseDisableDefaultArgs } from "../../util/cmd.ts";
import { exec } from "./command.ts";

type Range = readonly [number, number];

const isRange = is.TupleOf([is.Number, is.Number] as const);

export function main(denops: Denops): void {
denops.dispatcher = {
...denops.dispatcher,
"browse:command": (args, range) => {
assert(args, is.ArrayOf(is.String), { message: "args must be string[]" });
assert(range, is.OneOf([is.Undefined, isRange]), {
message: "range must be undefined | [number, number]",
});
const [disableDefaultArgs, realArgs] = parseDisableDefaultArgs(args);
return helper.friendlyCall(
denops,
() =>
command(denops, realArgs, {
disableDefaultArgs,
range,
}),
);
},
};
}

type CommandOptions = {
disableDefaultArgs?: boolean;
range?: Range;
};

async function command(
denops: Denops,
args: string[],
options: CommandOptions = {},
): Promise<void> {
if (!options.disableDefaultArgs) {
const defaultArgs = await vars.g.get(
denops,
"gin_browse_default_args",
[],
);
assert(defaultArgs, is.ArrayOf(is.String), {
message: "g:gin_browse_default_args must be string[]",
});
args = [...defaultArgs, ...args];
}
const [opts, flags, residue] = parse(await normCmdArgs(denops, args));
validateFlags(flags, [
"remote",
"permalink",
"path",
"home",
"commit",
"pr",
"n",
"no-browser",
]);
validateOpts(opts, [
"worktree",
"yank",
]);
const commitish = parseResidue(residue);
const path = unnullish(
await ensurePath(denops, opts.path),
(p) => formatPath(p, options.range),
);
await exec(denops, commitish ?? "HEAD", {
worktree: opts.worktree,
yank: "yank" in opts,
noBrowser: ("n" in flags || "no-browser" in flags),
remote: ensure(flags.remote, is.OneOf([is.Undefined, is.String]), {
"message": "REMOTE in --remote={REMOTE} must be string",
}),
path,
home: "home" in flags,
commit: "commit" in flags,
pr: "pr" in flags,
permalink: "permalink" in flags,
});
}

function parseResidue(
residue: string[],
): string | undefined {
// GinBrowse [{options}] {commitish}
switch (residue.length) {
case 0:
return undefined;
case 1:
return residue[0];
default:
throw new Error("Invalid number of arguments");
}
}

async function ensurePath(
denops: Denops,
path?: string,
): Promise<string | undefined> {
if (path) {
return ensure(path, is.String, {
message: "PATH in --path={PATH} must be string",
});
}
const [bufname, buftype, cwd] = await batch.collect(denops, (denops) => [
fn.expand(denops, "%:p") as Promise<string>,
fn.getbufvar(denops, "%", "&buftype") as Promise<string>,
fn.getcwd(denops),
]);
return buftype ? cwd : bufname;
}

function formatPath(path: string, range?: Range): string {
if (!range) {
return path;
}
const [start, end] = range;
if (start === end) {
return `${path}:${start}`;
}
return `${path}:${start}:${end}`;
}
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
49 changes: 49 additions & 0 deletions doc/gin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,39 @@ 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}...] [{flags}] [{commitish}]
Open a system browser to visit the hosting service webpage of the
repository. If no {commitish} is given, it defaults to HEAD.

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

++yank
Yank the URL to the clipboard.

See |gin-commands-options| for common {++option}.

The following flags are valid as {flags}:

--commit
--home
-n, --no-browser
--path={PATH}
--permalink
--pr
--remote={REMOTE}

It uses "git-browse" command as a module internally so see usage of
that for detail about each {flags}.

https://deno.land/x/git_browse

See |g:gin_browse_aliases| to define aliases of REMOTE to support
arbitrary domain (e.g. GitHub Enterprise).

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

*:GinCd*
Expand Down Expand Up @@ -445,6 +478,22 @@ VARIABLES *gin-variables*

Default: 0

*g:gin_browse_aliases*
Define a REMOTE alias for a specific hosting service on |:GinBrowse|
command. This is useful for example GitHub Enterprise with a custom
domain like
>
let g:gin_browse_aliases = {
\ 'github.on.my.custom.domain.com': 'github.com',
\}
<
Default: {}

*g:gin_browse_default_args*
Specify default arguments of |:GinBrowse|.

Default: []

*g:gin_chaperon_default_args*
Specify default arguments of |:GinChaperon|.

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

function! s:command(args, range, range_given) abort
let l:Callback = function('denops#notify', [
\ 'gin',
\ 'browse:command',
\ a:range_given ? [a:args, a:range] : [a:args],
\])
call denops#plugin#wait_async('gin', l:Callback)
endfunction

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

0 comments on commit 63904d7

Please sign in to comment.