Skip to content
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

feat: added svnfs to allow asynchronous log search #798

Merged
merged 12 commits into from
Feb 12, 2020
59 changes: 42 additions & 17 deletions src/commands/search_log_by_text.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as path from "path";
import { Command } from "./command";
import { window, Uri, commands } from "vscode";
import { window, Uri, commands, ProgressLocation } from "vscode";
import { Repository } from "../repository";
import { toSvnUri } from "../uri";
import { SvnUriAction } from "../common/types";
import * as cp from "child_process";
import { tempSvnFs } from "../temp_svn_fs";

export class SearchLogByText extends Command {
constructor() {
Expand All @@ -16,20 +15,46 @@ export class SearchLogByText extends Command {
return;
}

try {
const resource = toSvnUri(
Uri.file(repository.workspaceRoot),
SvnUriAction.LOG_SEARCH,
{ search: input }
);
const uri = resource.with({
path: path.join(resource.path, "svn.log")
const uri = Uri.parse("svnfs:/svn.log");
tempSvnFs.writeFile(uri, Buffer.from(""), {
create: true,
overwrite: true
});

await commands.executeCommand<void>("vscode.open", uri);

const proc = cp.spawn("svn", ["log", "--search", input], {
cwd: repository.workspaceRoot
});

let content = "";

proc.stdout.on("data", data => {
content += data.toString();

tempSvnFs.writeFile(uri, Buffer.from(content), {
create: true,
overwrite: true
});
});

await commands.executeCommand<void>("vscode.open", uri);
} catch (error) {
console.error(error);
window.showErrorMessage("Unable to log");
}
window.withProgress(
{
cancellable: true,
location: ProgressLocation.Notification,
title: "Searching Log"
},
(_progress, token) => {
token.onCancellationRequested(() => {
proc.kill("SIGINT");
});

return new Promise((resolve, reject) => {
proc.on("exit", (code: number) => {
code === 0 ? resolve() : reject();
});
});
}
);
}
}
3 changes: 2 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { toDisposable } from "./util";
import { BranchChangesProvider } from "./historyView/branchChangesProvider";
import { IsSvn19orGreater } from "./contexts/isSvn19orGreater";
import { IsSvn18orGreater } from "./contexts/isSvn18orGreater";
import { tempSvnFs } from "./temp_svn_fs";

async function init(
_context: ExtensionContext,
Expand All @@ -43,7 +44,7 @@ async function init(

registerCommands(sourceControlManager, disposables);

disposables.push(sourceControlManager, contentProvider);
disposables.push(sourceControlManager, contentProvider, tempSvnFs);

const svnProvider = new SvnProvider(sourceControlManager);

Expand Down
262 changes: 262 additions & 0 deletions src/temp_svn_fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import {
FileSystemProvider,
Event,
FileChangeEvent,
EventEmitter,
Uri,
Disposable,
FileStat,
FileType,
FileSystemError,
FileChangeType,
workspace
} from "vscode";
import * as path from "path";

export class File implements FileStat {
type: FileType;
ctime: number;
mtime: number;
size: number;
name: string;
data?: Uint8Array;

constructor(name: string) {
this.type = FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
}
}

export class Directory implements FileStat {
type: FileType;
ctime: number;
mtime: number;
size: number;

name: string;
entries: Map<string, File | Directory>;

constructor(name: string) {
this.type = FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
this.entries = new Map();
}
}

export type Entry = File | Directory;

class TempSvnFs implements FileSystemProvider, Disposable {
private _emitter = new EventEmitter<FileChangeEvent[]>();
private _bufferedEvents: FileChangeEvent[] = [];
private _fireSoonHandler?: NodeJS.Timer;
private _root = new Directory("");
private _disposables: Disposable[] = [];

readonly onDidChangeFile: Event<FileChangeEvent[]> = this._emitter.event;

constructor() {
this._disposables.push(
workspace.registerFileSystemProvider("tempsvnfs", this, {
isCaseSensitive: true
})
);
}

watch(_resource: Uri): Disposable {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return new Disposable(() => {});
}

stat(uri: Uri): FileStat {
return this._lookup(uri, false);
}

readDirectory(uri: Uri): [string, FileType][] {
const entry = this._lookupAsDirectory(uri, false);
const result: [string, FileType][] = [];
for (const [name, child] of entry.entries) {
result.push([name, child.type]);
}

return result;
}

createDirectory(uri: Uri): void {
const basename = path.posix.basename(uri.path);
const dirname = uri.with({ path: path.posix.dirname(uri.path) });
const parent = this._lookupAsDirectory(dirname, false);

const entry = new Directory(basename);
parent.entries.set(entry.name, entry);
parent.mtime = Date.now();
parent.size += 1;

this._fireSoon(
{ type: FileChangeType.Changed, uri: dirname },
{ type: FileChangeType.Created, uri }
);
}

readFile(uri: Uri): Uint8Array {
const data = this._lookupAsFile(uri, false).data;
if (data) {
return data;
}

throw FileSystemError.FileNotFound();
}

writeFile(
uri: Uri,
content: Uint8Array,
options: { create: boolean; overwrite: boolean }
): void {
const basename = path.posix.basename(uri.path);
const parent = this._lookupParentDirectory(uri);
let entry = parent.entries.get(basename);
if (entry instanceof Directory) {
throw FileSystemError.FileIsADirectory(uri);
}

if (!entry && !options.create) {
throw FileSystemError.FileNotFound(uri);
}

if (entry && options.create && !options.overwrite) {
throw FileSystemError.FileExists(uri);
}

if (!entry) {
entry = new File(basename);
parent.entries.set(basename, entry);
this._fireSoon({ type: FileChangeType.Created, uri });
}

entry.mtime = Date.now();
entry.size = content.byteLength;
entry.data = content;

this._fireSoon({ type: FileChangeType.Changed, uri });
}

delete(uri: Uri): void {
const dirname = uri.with({ path: path.posix.dirname(uri.path) });
const basename = path.posix.basename(uri.path);
const parent = this._lookupAsDirectory(dirname, false);
if (!parent.entries.has(basename)) {
throw FileSystemError.FileNotFound(uri);
}
parent.entries.delete(basename);
parent.mtime = Date.now();
parent.size -= 1;

this._fireSoon(
{ type: FileChangeType.Changed, uri: dirname },
{ type: FileChangeType.Deleted, uri }
);
}

rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void {
if (!options.overwrite && this._lookup(newUri, true)) {
throw FileSystemError.FileExists(newUri);
}

const entry = this._lookup(oldUri, false);
const oldParent = this._lookupParentDirectory(oldUri);

const newParent = this._lookupParentDirectory(newUri);
const newName = path.posix.basename(newUri.path);

oldParent.entries.delete(entry.name);
entry.name = newName;
newParent.entries.set(newName, entry);

this._fireSoon(
{ type: FileChangeType.Deleted, uri: oldUri },
{ type: FileChangeType.Created, uri: newUri }
);
}

dispose(): void {
this._disposables.forEach(disposable => disposable.dispose());
this._disposables = [];

for (const [name] of this.readDirectory(Uri.parse("svnfs:/"))) {
this.delete(Uri.parse(`svnfs:/${name}`));
}
}

private _lookup(uri: Uri, silent: false): Entry;
private _lookup(uri: Uri, silent: boolean): Entry | undefined;
private _lookup(uri: Uri, silent: boolean): Entry | undefined {
const parts = uri.path.split("/");
let entry: Entry = this._root;

for (const part of parts) {
if (!part) {
continue;
}

let child: Entry | undefined;
if (entry instanceof Directory) {
child = entry.entries.get(part);
}

if (!child) {
if (!silent) {
throw FileSystemError.FileNotFound(uri);
} else {
return undefined;
}
}

entry = child;
}

return entry;
}

private _lookupAsDirectory(uri: Uri, silent: boolean): Directory {
const entry = this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}

throw FileSystemError.FileNotADirectory(uri);
}

private _lookupAsFile(uri: Uri, silent: boolean): File {
const entry = this._lookup(uri, silent);
if (entry instanceof File) {
return entry;
}

throw FileSystemError.FileIsADirectory(uri);
}

private _lookupParentDirectory(uri: Uri): Directory {
const dirname = uri.with({ path: path.posix.dirname(uri.path) });
return this._lookupAsDirectory(dirname, false);
}

private _fireSoon(...events: FileChangeEvent[]): void {
this._bufferedEvents.push(...events);

if (this._fireSoonHandler) {
clearTimeout(this._fireSoonHandler);
}

this._fireSoonHandler = setTimeout(() => {
this._emitter.fire(this._bufferedEvents);
this._bufferedEvents.length = 0;
}, 1);
}
}

export const tempSvnFs = new TempSvnFs();