Skip to content

Commit

Permalink
Adds edit & commit support (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Amodio committed Jun 25, 2020
1 parent c9dc040 commit 789f0b0
Show file tree
Hide file tree
Showing 10 changed files with 1,350 additions and 219 deletions.
107 changes: 106 additions & 1 deletion extensions/github-browser/package.json
Expand Up @@ -13,11 +13,98 @@
"Other"
],
"activationEvents": [
"onFileSystem:github"
"onFileSystem:codespace",
"onFileSystem:github",
"onCommand:githubBrowser.openRepository"
],
"browser": "./dist/extension.js",
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "githubBrowser.openRepository",
"title": "Open GitHub Repository...",
"category": "GitHub Browser"
},
{
"command": "githubBrowser.commit",
"title": "Commit",
"icon": "$(check)",
"category": "GitHub Browser"
},
{
"command": "githubBrowser.discardChanges",
"title": "Discard Changes",
"icon": "$(discard)",
"category": "GitHub Browser"
},
{
"command": "githubBrowser.openChanges",
"title": "Open Changes",
"icon": "$(git-compare)",
"category": "GitHub Browser"
},
{
"command": "githubBrowser.openFile",
"title": "Open File",
"icon": "$(go-to-file)",
"category": "GitHub Browser"
}
],
"menus": {
"commandPalette": [
{
"command": "githubBrowser.commit",
"when": "github.hasChanges"
},
{
"command": "githubBrowser.discardChanges",
"when": "false"
},
{
"command": "githubBrowser.openChanges",
"when": "false"
},
{
"command": "githubBrowser.openFile",
"when": "false"
}
],
"scm/title": [
{
"command": "githubBrowser.commit",
"group": "navigation",
"when": "scmProvider == github"
}
],
"scm/resourceState/context": [
{
"command": "githubBrowser.openFile",
"when": "scmProvider == github && scmResourceGroup == github.changes",
"group": "inline@0"
},
{
"command": "githubBrowser.discardChanges",
"when": "scmProvider == github && scmResourceGroup == github.changes",
"group": "inline@1"
},
{
"command": "githubBrowser.openChanges",
"when": "scmProvider == github && scmResourceGroup == github.changes",
"group": "navigation@0"
},
{
"command": "githubBrowser.openFile",
"when": "scmProvider == github && scmResourceGroup == github.changes",
"group": "navigation@1"
},
{
"command": "githubBrowser.discardChanges",
"when": "scmProvider == github && scmResourceGroup == github.changes",
"group": "1_modification@0"
}
]
},
"resourceLabelFormatters": [
{
"scheme": "github",
Expand All @@ -36,6 +123,24 @@
"separator": "/",
"workspaceSuffix": "GitHub"
}
},
{
"scheme": "codespace",
"authority": "HEAD",
"formatting": {
"label": "github.com${path}",
"separator": "/",
"workspaceSuffix": "GitHub"
}
},
{
"scheme": "codespace",
"authority": "*",
"formatting": {
"label": "github.com${path} (${authority})",
"separator": "/",
"workspaceSuffix": "GitHub"
}
}
]
},
Expand Down
57 changes: 53 additions & 4 deletions extensions/github-browser/src/extension.ts
Expand Up @@ -3,9 +3,58 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { GitHubFS } from './githubfs';
import { commands, ExtensionContext, Uri, workspace, window } from 'vscode';
import { ChangeStore, ContextStore } from './stores';
import { VirtualFS } from './fs';
import { GitHubApiContext, GitHubApi } from './github/api';
import { GitHubFS } from './github/fs';
import { VirtualSCM } from './scm';

export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(new GitHubFS());
const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\W\/]+)\/([^\W\/]+?)(?:\/|.git|$)/i;

export function activate(context: ExtensionContext) {
const contextStore = new ContextStore<GitHubApiContext>(context.workspaceState, GitHubFS.scheme);
const changeStore = new ChangeStore(context.workspaceState);

const githubApi = new GitHubApi(contextStore);
const gitHubFS = new GitHubFS(githubApi);
const virtualFS = new VirtualFS('codespace', GitHubFS.scheme, contextStore, changeStore, gitHubFS);

context.subscriptions.push(
githubApi,
gitHubFS,
virtualFS,
new VirtualSCM(GitHubFS.scheme, githubApi, changeStore)
);

commands.registerCommand('githubBrowser.openRepository', async () => {
const value = await window.showInputBox({
placeHolder: 'e.g. https://github.com/microsoft/vscode',
prompt: 'Enter a GitHub repository url',
validateInput: value => repositoryRegex.test(value) ? undefined : 'Invalid repository url'
});

if (value) {
const match = repositoryRegex.exec(value);
if (match) {
const [, owner, repo] = match;

const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`);
openWorkspace(uri, repo, 'currentWindow');
}
}
});
}

export function getRootUri(uri: Uri) {
return workspace.getWorkspaceFolder(uri)?.uri;
}

function openWorkspace(uri: Uri, name: string, location: 'currentWindow' | 'newWindow' | 'addToCurrentWorkspace') {
if (location === 'addToCurrentWorkspace') {
const count = (workspace.workspaceFolders && workspace.workspaceFolders.length) || 0;
return workspace.updateWorkspaceFolders(count, 0, { uri: uri, name: name });
}

return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow');
}
180 changes: 180 additions & 0 deletions extensions/github-browser/src/fs.ts
@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

'use strict';
import {
CancellationToken,
Disposable,
Event,
EventEmitter,
FileChangeEvent,
FileChangeType,
FileSearchOptions,
FileSearchProvider,
FileSearchQuery,
FileStat,
FileSystemError,
FileSystemProvider,
FileType,
Progress,
TextSearchOptions,
TextSearchProvider,
TextSearchQuery,
TextSearchResult,
Uri,
workspace,
} from 'vscode';
import { IChangeStore, ContextStore } from './stores';
import { GitHubApiContext } from './github/api';

const emptyDisposable = { dispose: () => { /* noop */ } };
const textEncoder = new TextEncoder();

export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable {
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
get onDidChangeFile(): Event<FileChangeEvent[]> {
return this._onDidChangeFile.event;
}

private readonly disposable: Disposable;

constructor(
readonly scheme: string,
private readonly originalScheme: string,
contextStore: ContextStore<GitHubApiContext>,
private readonly changeStore: IChangeStore,
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
) {
// TODO@eamodio listen for workspace folder changes
for (const folder of workspace.workspaceFolders ?? []) {
const uri = this.getOriginalResource(folder.uri);

// If we have a saved context, but no longer have any changes, reset the context
// We only do this on startup/reload to keep things consistent
if (contextStore.get(uri) !== undefined && !changeStore.hasChanges(folder.uri)) {
contextStore.delete(uri);
}
}

this.disposable = Disposable.from(
workspace.registerFileSystemProvider(scheme, this, {
isCaseSensitive: true,
}),
workspace.registerFileSearchProvider(scheme, this),
workspace.registerTextSearchProvider(scheme, this),
changeStore.onDidChange(e => {
switch (e.type) {
case 'created':
this._onDidChangeFile.fire([{ type: FileChangeType.Created, uri: e.uri }]);
break;
case 'changed':
this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: e.uri }]);
break;
case 'deleted':
this._onDidChangeFile.fire([{ type: FileChangeType.Deleted, uri: e.uri }]);
break;
case 'renamed':
this._onDidChangeFile.fire([{ type: FileChangeType.Deleted, uri: e.originalUri }, { type: FileChangeType.Created, uri: e.uri }]);
break;
}
}),
);
}

dispose() {
this.disposable?.dispose();
}

private getOriginalResource(uri: Uri): Uri {
return uri.with({ scheme: this.originalScheme });
}

//#region FileSystemProvider

watch(): Disposable {
return emptyDisposable;
}

async stat(uri: Uri): Promise<FileStat> {
const content = this.changeStore.getContent(uri);
if (content !== undefined) {
return {
type: FileType.File,
size: textEncoder.encode(content).byteLength,
ctime: 0,
mtime: 0,
};
}

if (uri.path === '' || uri.path.lastIndexOf('/') === 0) {
return { type: FileType.Directory, size: 0, ctime: 0, mtime: 0 };
}

const stat = await this.fs.stat(this.getOriginalResource(uri));
return stat;
}

async readDirectory(uri: Uri): Promise<[string, FileType][]> {
const entries = await this.fs.readDirectory(this.getOriginalResource(uri));
return entries;
}

createDirectory(_uri: Uri): void | Thenable<void> {
throw FileSystemError.NoPermissions;
}

async readFile(uri: Uri): Promise<Uint8Array> {
const content = this.changeStore.getContent(uri);
if (content !== undefined) {
return textEncoder.encode(content);
}

const data = await this.fs.readFile(this.getOriginalResource(uri));
return data;
}

async writeFile(uri: Uri, content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
await this.changeStore.recordFileChange(uri, content, () => this.fs.readFile(this.getOriginalResource(uri)));
}

delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions;
}

rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions;
}

copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
throw FileSystemError.NoPermissions;
}

//#endregion

//#region FileSearchProvider

provideFileSearchResults(
query: FileSearchQuery,
options: FileSearchOptions,
token: CancellationToken,
) {
return this.fs.provideFileSearchResults(query, { ...options, folder: this.getOriginalResource(options.folder) }, token);
}

//#endregion

//#region TextSearchProvider

provideTextSearchResults(
query: TextSearchQuery,
options: TextSearchOptions,
progress: Progress<TextSearchResult>,
token: CancellationToken,
) {
return this.fs.provideTextSearchResults(query, { ...options, folder: this.getOriginalResource(options.folder) }, progress, token);
}

//#endregion
}

0 comments on commit 789f0b0

Please sign in to comment.