Skip to content

applyPatch action #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 30, 2023
Merged
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
152 changes: 113 additions & 39 deletions denops/@ddu-sources/git_diff.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { parseDiff } from "./udiff/diff.ts";
import * as stdpath from "https://deno.land/std@0.193.0/path/mod.ts";
import { ActionData } from "https://deno.land/x/ddu_kind_file@v0.5.2/file.ts";
import { applyPatch, DiffLine, parseDiff } from "./udiff/diff.ts";
import { groupBy } from "https://deno.land/std@0.195.0/collections/group_by.ts";
import * as stdpath from "https://deno.land/std@0.195.0/path/mod.ts";
import { ActionData } from "https://deno.land/x/ddu_kind_file@v0.5.3/file.ts";
import {
GatherArguments,
OnInitArguments,
} from "https://deno.land/x/ddu_vim@v3.4.1/base/source.ts";
} from "https://deno.land/x/ddu_vim@v3.4.3/base/source.ts";
import {
ActionArguments,
ActionFlags,
BaseSource,
Item,
ItemHighlight,
} from "https://deno.land/x/ddu_vim@v3.4.1/types.ts";
} from "https://deno.land/x/ddu_vim@v3.4.3/types.ts";
import { Denops } from "https://deno.land/x/denops_std@v5.0.1/mod.ts";
import * as u from "https://deno.land/x/unknownutil@v3.2.0/mod.ts";

type _ActionData = ActionData & {
_git_diff: number; // hack: suppress preview window closer
Expand All @@ -18,9 +22,34 @@ type _ActionData = ActionData & {
const defaultParams = {
cached: false,
onlyFile: false,
reverse: false,
show: false,
};

type Data = {
git_diff: {
line: DiffLine;
worktree: string;
path: string;
};
};

const isData: u.Predicate<Data> = u.isObjectOf({
git_diff: u.isObjectOf({
line: u.isObjectOf({
text: u.isString,
linum: u.isNumber,
olinum: u.isNumber,
}),
worktree: u.isString,
path: u.isString,
}),
});

const isItemWithData = u.isObjectOf({
data: isData,
});

type Params = typeof defaultParams;

const hls: Record<string, string> = {
Expand All @@ -29,10 +58,7 @@ const hls: Record<string, string> = {
"@": "diffLine",
};

const run = async (cmd: string[], cwd?: string): Promise<string> => {
if (cwd == null) {
cwd = Deno.cwd();
}
const run = async (cmd: string[], cwd = Deno.cwd()): Promise<string> => {
const proc = new Deno.Command(cmd[0], {
args: cmd.slice(1),
cwd,
Expand All @@ -41,45 +67,85 @@ const run = async (cmd: string[], cwd?: string): Promise<string> => {
return new TextDecoder().decode(stdout);
};

async function getWorktreeFromPath(denops: Denops, worktree: string) {
const type = await Deno.stat(worktree)
.then((info) => info.isFile ? "file" : "dir")
.catch(() => "nil");
let dir: string;
switch (type) {
case "file":
dir = stdpath.dirname(worktree);
break;
case "dir":
dir = worktree;
break;
default:
dir = String(await denops.call("getcwd"));
}
return (await run([
"git",
"rev-parse",
"--show-toplevel",
], dir)).trim();
}

export class Source extends BaseSource<Params> {
kind = "file";
override kind = "file";

private worktree = "";
override actions = {
applyPatch: async (args: ActionArguments<Params>) => {
const isNonNull = <T>(x: T): x is NonNullable<T> => x != null;
// ファイル単位で処理
const itemsByFiles = groupBy(
args.items
.map((item) => u.maybe(item, isItemWithData))
.filter(isNonNull), // 型ァ!(filter直だと上手く行かん)
(item) => item.data.git_diff.path,
);
for (const [abspath, items] of Object.entries(itemsByFiles)) {
// 型ァ!
if (items == null) {
continue;
}
const worktree = items[0].data.git_diff.worktree;
const patches = items.map((item) => item.data.git_diff.line);
// git showにtreeishを与えるとindexのデータを取れる
// treeishはworktree相対で与える必要がある
const path = stdpath.relative(worktree, abspath);
const result = (await run([
"git",
"show",
":" + path,
], worktree))
.split(/\n/g);
const patched = applyPatch(result, patches).join("\n");

override async onInit({
denops,
sourceOptions,
}: OnInitArguments<Params>): Promise<void> {
const worktree = String(sourceOptions.path);
const type = await Deno.stat(worktree)
.then((info) => info.isFile ? "file" : "dir")
.catch(() => "nil");
let dir: string;
switch (type) {
case "file":
dir = stdpath.dirname(worktree);
break;
case "dir":
dir = worktree;
break;
default:
dir = String(await denops.call("getcwd"));
}
this.worktree = (await run([
"git",
"rev-parse",
"--show-toplevel",
], dir)).trim();
}
// patch algorithm from lambdalisue/gin.vim
const renamed = abspath + Math.random();
await Deno.rename(abspath, renamed);
try {
await Deno.copyFile(renamed, abspath); // copy filemode etc...
await Deno.writeTextFile(abspath, patched);
await run(["git", "add", path], worktree);
} finally {
await Deno.remove(abspath).catch(console.log);
await Deno.rename(renamed, abspath);
}
}
return ActionFlags.RefreshItems;
},
};

gather({
denops,
sourceParams,
sourceOptions,
}: GatherArguments<Params>): ReadableStream<Item<_ActionData>[]> {
return new ReadableStream({
start: async (controller) => {
try {
const path = String(sourceOptions.path);
const worktree = await getWorktreeFromPath(denops, path);
const diff = (await run(
[
"git",
Expand All @@ -88,17 +154,18 @@ export class Source extends BaseSource<Params> {
"--no-prefix",
"--no-relative",
"--no-renames",
...(sourceParams.reverse ? ["-R"] : []),
...((!sourceParams.show && sourceParams.cached)
? ["--cached"]
: []),
...(sourceParams.onlyFile ? [path] : []),
],
this.worktree,
worktree,
)).split("\n");
const chunks = parseDiff(diff);
const items: Item<_ActionData>[][] = [];
for (const chunk of chunks) {
const fileName = stdpath.join(this.worktree, chunk.fileName);
const fileName = stdpath.join(worktree, chunk.fileName);
items.push(chunk.header.map((line, idx) => {
const hl = line.startsWith("---")
? "diffOldFile"
Expand Down Expand Up @@ -133,6 +200,13 @@ export class Source extends BaseSource<Params> {
});
}
return {
data: {
git_diff: {
line,
worktree: worktree,
path: fileName,
},
} satisfies Data,
word: line.text,
action: {
lineNr: line.linum,
Expand Down
48 changes: 41 additions & 7 deletions denops/@ddu-sources/udiff/diff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type DiffLine = {
text: string;
linum: number;
export type DiffLine = {
text: string; // text of line
linum: number; // line number of patch target file
olinum: number; // line number of origin file
};

export type DiffData = {
Expand All @@ -9,6 +10,8 @@ export type DiffData = {
lines: DiffLine[];
};

const hunkAddressExpr = /-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/;

export const splitAtFile = (lines: string[]): string[][][] => {
let ptr = 0;
const files: string[][][] = [];
Expand All @@ -19,7 +22,8 @@ export const splitAtFile = (lines: string[]): string[][][] => {
while (lines[ptr][0] === "@") {
const hunkStart = ptr;
// Note: hunk size omitted if 1
const m = lines[ptr].match(/-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/);
//
const m = lines[ptr].match(hunkAddressExpr);
if (m == null) {
throw "invalid hunk at " + ptr;
}
Expand Down Expand Up @@ -59,24 +63,54 @@ export const splitAtFile = (lines: string[]): string[][][] => {

export const parseHunk = (lines: string[]): DiffLine[] => {
const parsed: DiffLine[] = [];
const m = lines[0].match(/\+(\d+)/);
const m = lines[0].match(hunkAddressExpr);
if (m == null) {
throw Error("m == null");
}
let linum = parseInt(m[0]);
let olinum = parseInt(m[1] ?? 1);
let linum = parseInt(m[3] ?? 1);
parsed.push({
text: lines[0],
linum,
olinum,
});
for (let i = 1; i < lines.length; i++) {
parsed.push({
text: lines[i],
linum: lines[i].startsWith("-") ? linum : linum++,
linum,
olinum,
});
const head = lines[i][0];
if (head === "+") {
linum++;
} else if (head === "-") {
olinum++;
} else {
linum++;
olinum++;
}
}
return parsed;
};

export function applyPatch(origin: string[], patch: DiffLine[]): string[] {
const patched = origin.slice();
// 簡素化のため、後ろからパッチを打つ
// 順番狂うので+(linum)を先に処理する
const realPatch = patch.toSorted((a, b) =>
b.linum - a.linum || b.olinum - a.olinum
);
for (const p of realPatch) {
const point = p.olinum - 1;
if (p.text[0] === "-") {
patched.splice(point, 1);
} else if (p.text[0] === "+") {
patched.splice(point, 0, p.text.slice(1));
}
}
return patched;
}

export const parseDiff = (lines: string[]): DiffData[] => {
const split = splitAtFile(lines);
const result: DiffData[] = [];
Expand Down
Loading