diff --git a/extensions/git-extended/.vscode/launch.json b/extensions/git-extended/.vscode/launch.json new file mode 100644 index 00000000000..017c8762415 --- /dev/null +++ b/extensions/git-extended/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/client/out/**/*.js"], + "preLaunchTask": "npm" + } + ] +} \ No newline at end of file diff --git a/extensions/git-extended/.vscode/tasks.json b/extensions/git-extended/.vscode/tasks.json new file mode 100644 index 00000000000..0a411c1c867 --- /dev/null +++ b/extensions/git-extended/.vscode/tasks.json @@ -0,0 +1,9 @@ +{ + "version": "0.1.0", + "command": "npm", + "isShellCommand": true, + "showOutput": "silent", + "args": ["run", "compile"], + "isBackground": true, + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/extensions/git-extended/.vscodeignore b/extensions/git-extended/.vscodeignore new file mode 100644 index 00000000000..d43a539fddf --- /dev/null +++ b/extensions/git-extended/.vscodeignore @@ -0,0 +1,2 @@ +src/** +tsconfig.json \ No newline at end of file diff --git a/extensions/git-extended/README.md b/extensions/git-extended/README.md new file mode 100644 index 00000000000..0cb79afb054 --- /dev/null +++ b/extensions/git-extended/README.md @@ -0,0 +1,5 @@ +# Git Extended + + +## Notices +Uses code from github desktop: https://github.com/desktop/desktop \ No newline at end of file diff --git a/extensions/git-extended/images/npm_icon.png b/extensions/git-extended/images/npm_icon.png new file mode 100644 index 00000000000..f7f18b560ee Binary files /dev/null and b/extensions/git-extended/images/npm_icon.png differ diff --git a/extensions/git-extended/package.json b/extensions/git-extended/package.json new file mode 100644 index 00000000000..dc3859758e0 --- /dev/null +++ b/extensions/git-extended/package.json @@ -0,0 +1,119 @@ +{ + "name": "git-extended", + "displayName": "Git Extended", + "description": "Git Extended", + "enableProposedApi": true, + "version": "0.0.1", + "publisher": "rebornix", + "engines": { + "vscode": "^1.13.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onView:commits", + "onView:stash", + "onView:pr" + ], + "main": "./out/extension", + "contributes": { + "views": { + "explorer": [ + { + "id": "pr", + "name": "Pull Requests" + } + ] + }, + "commands": [ + { + "command": "commits.revertCommit", + "title": "Revert", + "icon": { + "dark": "resources/icons/dark/clean.svg", + "light": "resources/icons/light/clean.svg" + } + }, + { + "command": "commits.refresh", + "title": "Refresh", + "icon": { + "dark": "resources/icons/dark/refresh.svg", + "light": "resources/icons/light/refresh.svg" + } + }, + { + "command": "pr.refreshList", + "title": "Refresh", + "icon": { + "dark": "resources/icons/dark/refresh.svg", + "light": "resources/icons/light/refresh.svg" + } + }, + { + "command": "stash.apply", + "title": "Apply Stash" + }, + { + "command": "stash.delete", + "title": "Delete Stash" + }, + { + "command": "stash.pop", + "title": "Pop Stash" + }, + { + "command": "stash.stash", + "title": "Stash", + "icon": { + "dark": "resources/icons/dark/stage.svg", + "light": "resources/icons/light/stage.svg" + } + } + ], + "menus": { + "view/title": [ + { + "command": "commits.refresh", + "when": "view == commits", + "group": "navigation" + }, + { + "command": "stash.stash", + "when": "view == stash", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "stash.apply", + "when": "view == stash" + }, + { + "command": "stash.pop", + "when": "view == stash" + }, + { + "command": "stash.delete", + "when": "view == stash" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "tsc -p ./", + "compile": "tsc -watch -p ./" + }, + "devDependencies": { + "typescript": "^2.1.4", + "@types/node": "*" + }, + "dependencies": { + "dugite": "^1.28.0", + "tmp": "^0.0.31", + "octokat": "^0.8.0", + "request": "^2.81.0", + "lodash": "4.17.5" + } +} diff --git a/extensions/git-extended/package.nls.json b/extensions/git-extended/package.nls.json new file mode 100644 index 00000000000..0a9ece35f6e --- /dev/null +++ b/extensions/git-extended/package.nls.json @@ -0,0 +1,11 @@ +{ + "description": "Extension to add task support for npm scripts.", + "displayName": "Npm support for VSCode", + "config.npm.autoDetect": "Controls whether auto detection of npm scripts is on or off. Default is on.", + "config.npm.runSilent": "Run npm commands with the `--silent` option.", + "config.npm.packageManager": "The package manager used to run scripts.", + "config.npm.exclude": "Configure glob patterns for folders that should be excluded from automatic script detection.", + "npm.parseError": "Npm task detection: failed to parse the file {0}", + "taskdef.script": "The npm script to customize.", + "taskdef.path": "The path to the folder of the package.json file that provides the script. Can be ommitted." +} diff --git a/extensions/git-extended/resources/icons/checked_checkbox.png b/extensions/git-extended/resources/icons/checked_checkbox.png new file mode 100644 index 00000000000..f8280bdfc32 Binary files /dev/null and b/extensions/git-extended/resources/icons/checked_checkbox.png differ diff --git a/extensions/git-extended/resources/icons/dark/check.svg b/extensions/git-extended/resources/icons/dark/check.svg new file mode 100644 index 00000000000..c225b2f597f --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/clean.svg b/extensions/git-extended/resources/icons/dark/clean.svg new file mode 100644 index 00000000000..9f175633389 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/clean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/git.svg b/extensions/git-extended/resources/icons/dark/git.svg new file mode 100644 index 00000000000..c08b1c2e403 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/git.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/open-change.svg b/extensions/git-extended/resources/icons/dark/open-change.svg new file mode 100644 index 00000000000..c951728abac --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/open-change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/open-file.svg b/extensions/git-extended/resources/icons/dark/open-file.svg new file mode 100644 index 00000000000..f6302185aa4 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/open-file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/refresh.svg b/extensions/git-extended/resources/icons/dark/refresh.svg new file mode 100644 index 00000000000..d79fdaa4e8e --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/stage.svg b/extensions/git-extended/resources/icons/dark/stage.svg new file mode 100644 index 00000000000..3475c1e1963 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/stage.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-added.svg b/extensions/git-extended/resources/icons/dark/status-added.svg new file mode 100644 index 00000000000..cdc40f45f16 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-added.svg @@ -0,0 +1,6 @@ + + + + A + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-conflict.svg b/extensions/git-extended/resources/icons/dark/status-conflict.svg new file mode 100644 index 00000000000..53b243c8b9f --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-conflict.svg @@ -0,0 +1,6 @@ + + + + C + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-copied.svg b/extensions/git-extended/resources/icons/dark/status-copied.svg new file mode 100644 index 00000000000..7bd78c9427e --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-copied.svg @@ -0,0 +1,6 @@ + + + + C + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-deleted.svg b/extensions/git-extended/resources/icons/dark/status-deleted.svg new file mode 100644 index 00000000000..e7596e2e2ad --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-deleted.svg @@ -0,0 +1,6 @@ + + + + D + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-ignored.svg b/extensions/git-extended/resources/icons/dark/status-ignored.svg new file mode 100644 index 00000000000..85abc367a29 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-ignored.svg @@ -0,0 +1,6 @@ + + + + I + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-modified.svg b/extensions/git-extended/resources/icons/dark/status-modified.svg new file mode 100644 index 00000000000..d0de37d3468 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-modified.svg @@ -0,0 +1,6 @@ + + + + M + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-renamed.svg b/extensions/git-extended/resources/icons/dark/status-renamed.svg new file mode 100644 index 00000000000..a77fb41179a --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-renamed.svg @@ -0,0 +1,6 @@ + + + + R + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/status-untracked.svg b/extensions/git-extended/resources/icons/dark/status-untracked.svg new file mode 100644 index 00000000000..c6a48e14f08 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/status-untracked.svg @@ -0,0 +1,6 @@ + + + + U + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/dark/unstage.svg b/extensions/git-extended/resources/icons/dark/unstage.svg new file mode 100644 index 00000000000..2de46fcf5b5 --- /dev/null +++ b/extensions/git-extended/resources/icons/dark/unstage.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/check.svg b/extensions/git-extended/resources/icons/light/check.svg new file mode 100644 index 00000000000..d45df06edf8 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/clean.svg b/extensions/git-extended/resources/icons/light/clean.svg new file mode 100644 index 00000000000..1fa6ba48a19 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/clean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/git.svg b/extensions/git-extended/resources/icons/light/git.svg new file mode 100644 index 00000000000..d1049a44d0d --- /dev/null +++ b/extensions/git-extended/resources/icons/light/git.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/open-change.svg b/extensions/git-extended/resources/icons/light/open-change.svg new file mode 100644 index 00000000000..3a205509bca --- /dev/null +++ b/extensions/git-extended/resources/icons/light/open-change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/open-file.svg b/extensions/git-extended/resources/icons/light/open-file.svg new file mode 100644 index 00000000000..fccdf83d467 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/open-file.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/refresh.svg b/extensions/git-extended/resources/icons/light/refresh.svg new file mode 100644 index 00000000000..e0345748192 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/stage.svg b/extensions/git-extended/resources/icons/light/stage.svg new file mode 100644 index 00000000000..bdecdb0e45b --- /dev/null +++ b/extensions/git-extended/resources/icons/light/stage.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-added.svg b/extensions/git-extended/resources/icons/light/status-added.svg new file mode 100644 index 00000000000..587fc08f5f5 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-added.svg @@ -0,0 +1,6 @@ + + + + A + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-conflict.svg b/extensions/git-extended/resources/icons/light/status-conflict.svg new file mode 100644 index 00000000000..b6088ecd083 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-conflict.svg @@ -0,0 +1,6 @@ + + + + C + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-copied.svg b/extensions/git-extended/resources/icons/light/status-copied.svg new file mode 100644 index 00000000000..151fdb2cdbe --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-copied.svg @@ -0,0 +1,6 @@ + + + + C + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-deleted.svg b/extensions/git-extended/resources/icons/light/status-deleted.svg new file mode 100644 index 00000000000..7ed166accfe --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-deleted.svg @@ -0,0 +1,6 @@ + + + + D + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-ignored.svg b/extensions/git-extended/resources/icons/light/status-ignored.svg new file mode 100644 index 00000000000..85abc367a29 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-ignored.svg @@ -0,0 +1,6 @@ + + + + I + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-modified.svg b/extensions/git-extended/resources/icons/light/status-modified.svg new file mode 100644 index 00000000000..ff338b81410 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-modified.svg @@ -0,0 +1,6 @@ + + + + M + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-renamed.svg b/extensions/git-extended/resources/icons/light/status-renamed.svg new file mode 100644 index 00000000000..a77fb41179a --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-renamed.svg @@ -0,0 +1,6 @@ + + + + R + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/status-untracked.svg b/extensions/git-extended/resources/icons/light/status-untracked.svg new file mode 100644 index 00000000000..c6a48e14f08 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/status-untracked.svg @@ -0,0 +1,6 @@ + + + + U + + \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/light/unstage.svg b/extensions/git-extended/resources/icons/light/unstage.svg new file mode 100644 index 00000000000..f5d128b2df8 --- /dev/null +++ b/extensions/git-extended/resources/icons/light/unstage.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/extensions/git-extended/resources/icons/unchecked_checkbox.png b/extensions/git-extended/resources/icons/unchecked_checkbox.png new file mode 100644 index 00000000000..849e857eb9b Binary files /dev/null and b/extensions/git-extended/resources/icons/unchecked_checkbox.png differ diff --git a/extensions/git-extended/src/commitsProvider.ts b/extensions/git-extended/src/commitsProvider.ts new file mode 100644 index 00000000000..fa27f122747 --- /dev/null +++ b/extensions/git-extended/src/commitsProvider.ts @@ -0,0 +1,219 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Repository } from './common//models/repository'; +import { getChangedFiles, getFile } from './common/file'; +import { getCommits } from './common/log'; +import { Commit } from './common/models/commit'; +import { GitChangeType } from './common/models/file'; + +export class CommitTreeItem implements vscode.TreeItem { + readonly iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; + + constructor( + public readonly context: vscode.ExtensionContext, + public readonly label: string, + public readonly sha: string, + public readonly parentSHAs: ReadonlyArray, + public readonly command?: vscode.Command, + public readonly collapsibleState?: vscode.TreeItemCollapsibleState + ) { + this.collapsibleState = 1; + } +} + +export class FileChangeItem implements vscode.TreeItem { + public iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; + public filePath: string; + public sha: string; + public parentFilePath: string; + public parentSha: string; + public command?: vscode.Command; + public comments?: any[]; + + constructor( + public readonly label: string, + public readonly status: GitChangeType, + public readonly context: vscode.ExtensionContext, + public readonly fileName: string, + public readonly workspaceRoot?: string + ) { + } + + public populateCommandArgs() { + if (this.status === GitChangeType.MODIFY) { + this.command = { + title: 'show diff', + command: ShowDiffCommand.id, + arguments: [this] + }; + } else if (this.status === GitChangeType.DELETE) { + this.command = { + title: 'show diff', + command: 'vscode.open', + arguments: [ + vscode.Uri.file(path.resolve(this.workspaceRoot, this.parentFilePath)) + ] + }; + } else { + this.command = { + title: 'show diff', + command: 'vscode.open', + arguments: [ + vscode.Uri.file(path.resolve(this.workspaceRoot, this.filePath)) + ] + }; + } + } +} + +class ShowDiffCommand { + static readonly id = 'msgit.showDiff'; + + static run(item: FileChangeItem) { + vscode.commands.executeCommand('vscode.diff', + vscode.Uri.file(path.resolve(item.workspaceRoot, item.parentFilePath)), + vscode.Uri.file(path.resolve(item.workspaceRoot, item.filePath)), + item.fileName); + } +} + +export class CommitsProvider implements vscode.TreeDataProvider { + private context: vscode.ExtensionContext; + private workspaceRoot: string; + private repository: Repository; + private icons: any; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + activate(context: vscode.ExtensionContext, workspaceRoot: string, repository: Repository) { + this.context = context; + this.workspaceRoot = workspaceRoot; + this.repository = repository; + + vscode.window.registerTreeDataProvider('commits', this); + this.icons = { + light: { + Modified: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-modified.svg')), + Added: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-added.svg')), + Deleted: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-deleted.svg')), + Renamed: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-renamed.svg')), + Copied: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-copied.svg')), + Untracked: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-untrackedt.svg')), + Ignored: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-ignored.svg')), + Conflict: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-conflict.svg')), + }, + dark: { + Modified: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-modified.svg')), + Added: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-added.svg')), + Deleted: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-deleted.svg')), + Renamed: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-renamed.svg')), + Copied: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-copied.svg')), + Untracked: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-untracked.svg')), + Ignored: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-ignored.svg')), + Conflict: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-conflict.svg')) + } + }; + + vscode.commands.registerCommand('commits.refresh', async (args) => { + this._onDidChangeTreeData.fire(); + }); + vscode.commands.registerCommand('commits.revertCommit', async (element) => { + // TODO + // We can want to allow users to revert several commits + }); + vscode.commands.registerCommand(ShowDiffCommand.id, ShowDiffCommand.run); + } + + getTreeItem(element: CommitTreeItem | FileChangeItem): vscode.TreeItem { + if (element instanceof FileChangeItem) { + let iconUri: string; + let iconDarkUri: string; + + switch (element.status) { + case GitChangeType.ADD: + iconUri = this.icons.light.Added; + iconDarkUri = this.icons.dark.Added; + break; + case GitChangeType.COPY: + iconUri = this.icons.light.Copied; + iconDarkUri = this.icons.dark.Copied; + break; + case GitChangeType.DELETE: + iconUri = this.icons.light.Deleted; + iconDarkUri = this.icons.dark.Deleted; + break; + case GitChangeType.MODIFY: + iconUri = this.icons.light.Modified; + iconDarkUri = this.icons.dark.Modified; + break; + case GitChangeType.RENAME: + iconUri = this.icons.light.Renamed; + iconDarkUri = this.icons.dark.Renamed; + break; + } + element.iconPath = { + light: iconUri, + dark: iconDarkUri + }; + return element; + } else { + return element; + } + } + + getChildren(element?: CommitTreeItem): Thenable { + if (!this.workspaceRoot) { + return Promise.resolve([]); + } + + return new Promise(resolve => { + if (element) { + getChangedFiles(this.repository, element.sha).then(fileChanges => { + let promises = []; + let results = fileChanges.map(fileChange => { + let changedItem = new FileChangeItem(`${fileChange.filePath}`, fileChange.status, this.context, this.workspaceRoot); + + if (fileChange.status === GitChangeType.MODIFY) { + promises.push(Promise.all([getFile(element.sha, fileChange.filePath).then(targetFile => { + changedItem.filePath = targetFile; + changedItem.sha = element.sha; + }), getFile(element.parentSHAs[0], fileChange.filePath).then(targetFile => { + changedItem.parentFilePath = targetFile; + changedItem.parentSha = element.parentSHAs[0]; + })]).then(() => { + changedItem.populateCommandArgs(); + })); + } else if (fileChange.status === GitChangeType.DELETE) { + promises.push(getFile(element.parentSHAs[0], fileChange.filePath).then(targetFile => { + changedItem.parentFilePath = targetFile; + changedItem.sha = element.parentSHAs[0]; + changedItem.populateCommandArgs(); + })); + } else { + promises.push(getFile(element.sha, fileChange.filePath).then(targetFile => { + changedItem.filePath = targetFile; + changedItem.sha = element.sha; + changedItem.populateCommandArgs(); + })); + } + + return changedItem; + }); + + Promise.all(promises).then(() => { + resolve(results as any); // TODO + }); + }); + } else { + getCommits(this.repository, 'HEAD', 100).then((commits: Commit[]) => { + let result = commits.map(commit => { + return new CommitTreeItem(this.context, `${commit.summary}`, commit.sha, commit.parentSHAs); + }); + resolve(result); + }, (reason: any) => { + Promise.reject(reason); + }); + } + }); + } +} diff --git a/extensions/git-extended/src/common/diff.ts b/extensions/git-extended/src/common/diff.ts new file mode 100644 index 00000000000..8df56f3268a --- /dev/null +++ b/extensions/git-extended/src/common/diff.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import { getFileContent, writeTmpFile } from './file'; +import { GitChangeType, RichFileChange } from './models/file'; +import { Repository } from './models/repository'; + +export const MODIFY_DIFF_INFO = /diff --git a\/(\S+) b\/(\S+).*\n*index.*\n*-{3}.*\n*\+{3}.*\n*((.*\n*)+)/; +export const NEW_FILE_INFO = /diff --git a\/(\S+) b\/(\S+).*\n*new file mode .*\nindex.*\n*-{3}.*\n*\+{3}.*\n*((.*\n*)+)/; +export const DELETE_FILE_INFO = /diff --git a\/(\S+) b\/(\S+).*\n*deleted file mode .*\nindex.*\n*-{3}.*\n*\+{3}.*\n*((.*\n*)+)/; +export const DIFF_HUNK_INFO = /@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@/; + + +async function parseModifiedHunkComplete(originalContent, modifyDiffInfo, a, b) { + let left = originalContent.split(/\r|\n|\r\n/); + let diffHunks = modifyDiffInfo[3].split('\n'); + diffHunks.pop(); // there is one additional line break at the end of the diff ?? + + let right = []; + let lastCommonLine = 0; + for (let i = 0; i < diffHunks.length; i++) { + let line = diffHunks[i]; + if (DIFF_HUNK_INFO.test(line)) { + let changeInfo = DIFF_HUNK_INFO.exec(line); + let oriStartLine = Number(changeInfo[1]); + let oriEndLine = Number(changeInfo[3]) | 0; + + for (let j = lastCommonLine + 1; j < oriStartLine; j++) { + right.push(left[j - 1]); + } + lastCommonLine = oriStartLine + oriEndLine - 1; + } else if (/^\+/.test(line)) { + right.push(line.substr(1)); + } else { + let codeInFirstLine = line.substr(1); + right.push(codeInFirstLine); + } + } + + if (lastCommonLine < left.length) { + for (let j = lastCommonLine + 1; j <= left.length; j++) { + right.push(left[j - 1]); + } + } + + let contentPath = await writeTmpFile(right.join('\n'), path.extname(b)); + let originalContentPath = await writeTmpFile(left.join('\n'), path.extname(a)); + + return new RichFileChange(contentPath, originalContentPath, GitChangeType.MODIFY, b); +} + +async function parseModifiedHunkFast(modifyDiffInfo, a, b) { + let left = []; + let right = []; + + let diffHunks = modifyDiffInfo[3].split('\n'); + diffHunks.pop(); // there is one additional line break at the end of the diff ?? + + for (let i = 0; i < diffHunks.length; i++) { + let line = diffHunks[i]; + if (/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@/.test(line)) { + // let changeInfo = /@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@/.exec(line); + left.push(line); + right.push(line); + } else if (/^\-/.test(line)) { + left.push(line.substr(1)); + } else if (/^\+/.test(line)) { + right.push(line.substr(1)); + } else { + let codeInFirstLine = line.substr(1); + left.push(codeInFirstLine); + right.push(codeInFirstLine); + } + } + + let contentPath = await writeTmpFile(right.join('\n'), path.extname(b)); + let originalContentPath = await writeTmpFile(left.join('\n'), path.extname(a)); + + return new RichFileChange(contentPath, originalContentPath, GitChangeType.MODIFY, b); +} + +export async function parseDiff(text: string, repository: Repository, parentCommit: string) { + let reg = /diff((?!diff).*\n*)*/g; + let match = reg.exec(text); + let richFileChanges: RichFileChange[] = []; + + while(match) { + let singleFileDiff = match[0]; + let modifyDiffInfo = MODIFY_DIFF_INFO.exec(singleFileDiff); + if (modifyDiffInfo) { + let a = modifyDiffInfo[1]; + let b = modifyDiffInfo[2]; + + try { + let originalContent = await getFileContent(repository.path, parentCommit, a); + let richFileChange = await parseModifiedHunkComplete(originalContent, modifyDiffInfo, a, b); + richFileChanges.push(richFileChange); + } catch (e) { + let richFileChange = await parseModifiedHunkFast(modifyDiffInfo, a, b); + richFileChanges.push(richFileChange); + } + + match = reg.exec(text); + continue; + } + + let newDiffInfo = NEW_FILE_INFO.exec(singleFileDiff); + if (newDiffInfo) { + let fileName = newDiffInfo[1]; + let diffHunks = newDiffInfo[3].split('\n'); + let contentArray = []; + for (let i = 0; i < diffHunks.length; i++) { + if (/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@$/.test(diffHunks[i])) { + continue; + } else if (/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@ /.test(diffHunks[i])) { + contentArray.push(diffHunks[i].replace(/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@ /, '')); + } else if (/^\+/.test(diffHunks[i])) { + contentArray.push(diffHunks[i].substr(1)); + } + } + let filePath = await writeTmpFile(contentArray.join('\n'), path.extname(fileName)); + let richFileChange = new RichFileChange(filePath, filePath, GitChangeType.ADD, fileName); + richFileChanges.push(richFileChange); + match = reg.exec(text); + continue; + } + + let deleteDiffInfo = DELETE_FILE_INFO.exec(singleFileDiff); + if (deleteDiffInfo) { + let fileName = deleteDiffInfo[1]; + let diffHunks = deleteDiffInfo[3].split('\n'); + let contentArray = []; + for (let i = 0; i < diffHunks.length; i++) { + if (/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@$/.test(diffHunks[i])) { + continue; + } else if (/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@ /.test(diffHunks[i])) { + contentArray.push(diffHunks[i].replace(/@@ \-(\d+)(,(\d+))?( \+(\d+)(,(\d+)?))? @@ /, '')); + } else if (/^\-/.test(diffHunks[i])) { + contentArray.push(diffHunks[i].substr(1)); + } + } + let originalFilePath = await writeTmpFile(contentArray.join('\n'), path.extname(fileName)); + let filePath = await writeTmpFile('', path.extname(fileName)); + let richFileChange = new RichFileChange(filePath, originalFilePath, GitChangeType.DELETE, fileName); + richFileChanges.push(richFileChange); + match = reg.exec(text); + continue; + } + match = reg.exec(text); + } + + return richFileChanges; +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/file.ts b/extensions/git-extended/src/common/file.ts new file mode 100644 index 00000000000..b6405168628 --- /dev/null +++ b/extensions/git-extended/src/common/file.ts @@ -0,0 +1,107 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as tmp from 'tmp'; +import * as vscode from 'vscode'; +import { GitProcess } from 'dugite'; +import { Repository } from './models/repository'; +import { SlimFileChange, GitChangeType, fromStatus } from './models/file'; + + +/** + * @param content + * + * a + * + * c + * + * b + * + * + * @param ext + */ + +export async function writeTmpFile(content: string, ext: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({ postfix: ext }, async (err: any, tmpFilePath: string) => { + if (err) { + reject(err); + return; + } + + try { + fs.appendFileSync(tmpFilePath, content); + resolve(tmpFilePath); + } catch (ex) { + reject(ex); + } + }); + }) +} + +export async function getFile(commitSha1: string, localFilePath: string): Promise { + const rootDir = vscode.workspace.rootPath; + return new Promise((resolve, reject) => { + if (commitSha1 === undefined) { + resolve('fileUnavailable'); + return; + } + let ext = path.extname(localFilePath); + tmp.file({ postfix: ext }, async (err: any, tmpFilePath: string) => { + if (err) { + reject(err); + return; + } + try { + let data = await getFileContent(rootDir, commitSha1, localFilePath); + fs.appendFileSync(tmpFilePath, data); + resolve(tmpFilePath); + } + catch (ex) { + console.log(ex); + reject(ex); + } + }); + }); +} + +export async function getFileContent(rootDir: string, commitSha: string, sourceFilePath: string): Promise { + const result = await GitProcess.exec([ + 'show', + `${commitSha}:` + sourceFilePath.replace(/\\/g, '/') + ], rootDir); + + const out = result.stdout; + const error = result.stderr; + + if (result.exitCode === 0) { + return out; + } else { + throw error; + } +} + +export async function getChangedFiles(repository: Repository, sha: string): Promise> { + const args = ['log', sha, '--name-status', '--format=format:', '-z', '-1']; + const result = await GitProcess.exec(args, repository.path); + + const out = result.stdout; + const lines = out.split('\0'); + lines.splice(-1, 1); + + const files: SlimFileChange[] = []; + for (let i = 0; i < lines.length; i++) { + const statusText = lines[i]; + const status = fromStatus(statusText); + let originalPath: string | undefined = undefined; + + if (status === GitChangeType.RENAME || status === GitChangeType.COPY) { + originalPath = lines[++i]; + } + + const path = lines[++i]; + + files.push(new SlimFileChange(path, originalPath, status, null)); + } + + return files; +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/log.ts b/extensions/git-extended/src/common/log.ts new file mode 100644 index 00000000000..32352dc53fd --- /dev/null +++ b/extensions/git-extended/src/common/log.ts @@ -0,0 +1,77 @@ +import { Repository } from './models/repository'; +import { Commit } from './models/commit'; +import { GitProcess } from 'dugite'; + +export async function getParentCommit(repository: Repository, sha: string): Promise { + const result = await GitProcess.exec( + [ + 'rev-list', + '--parents', + '-n', + '1', + sha + ], + repository.path + ); + let commits = result.stdout.split(' '); + if (commits.length > 2) { + return commits[1]; + } else { + return null; + } +} + + + +export async function getCommits(repository: Repository, revisionRange: string, limit: number, additionalArgs: ReadonlyArray = []): Promise { + const delimiter = '1F'; + const delimiterString = String.fromCharCode(parseInt(delimiter, 16)); + const prettyFormat = [ + '%H', // SHA + '%s', // summary + '%P', // parent SHAs + ].join(`%x${delimiter}`); + + const result = await GitProcess.exec([ + 'log', + revisionRange, + `--max-count=${limit}`, + `--pretty=${prettyFormat}`, + '-z', + ...additionalArgs, + ], repository.path); + + const out = result.stdout; + const lines = out.split('\0'); + lines.splice(-1, 1); + + const commits = lines.map(line => { + const pieces = line.split(delimiterString); + const sha = pieces[0]; + const summary = pieces[1]; + const shaList = pieces[2]; + const parentSHAs = shaList.length ? shaList.split(' ') : []; + + return new Commit(sha, summary, parentSHAs); + }); + + return commits; +} + +export async function isWorkingTreeClean(repository: Repository): Promise { + const result = await GitProcess.exec( + [ + 'diff-index', + '--quiet', + 'HEAD', + '--' + ], + repository.path + ); + let exitCode = result.exitCode; + if (exitCode !== 0) { + return false; + } else { + return true; + } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/models/comment.ts b/extensions/git-extended/src/common/models/comment.ts new file mode 100644 index 00000000000..1654f0549b5 --- /dev/null +++ b/extensions/git-extended/src/common/models/comment.ts @@ -0,0 +1,29 @@ +interface DiffHunkRange { + originalStart: number; + originalLength: number; + start: number; + length: number; + +} + +interface User { + id: string; + login: string; +} +export interface Comment { + url: string; + id: string; + path: string; + pull_request_review_id: string; + diff_hunk_range: DiffHunkRange; + position: number; + originalPosition: number; + commit_id: string; + original_commit_id: string; + user: User; + body: string; + created_at: string; + updated_at: string; + html_url: string; +} + diff --git a/extensions/git-extended/src/common/models/commit.ts b/extensions/git-extended/src/common/models/commit.ts new file mode 100644 index 00000000000..ce83eda5b96 --- /dev/null +++ b/extensions/git-extended/src/common/models/commit.ts @@ -0,0 +1,3 @@ +export class Commit { + constructor(public sha: string, public summary: string, public parentSHAs: string[]) { } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/models/file.ts b/extensions/git-extended/src/common/models/file.ts new file mode 100644 index 00000000000..4e38d8138a1 --- /dev/null +++ b/extensions/git-extended/src/common/models/file.ts @@ -0,0 +1,49 @@ +export enum GitChangeType { + ADD, + COPY, + DELETE, + MODIFY, + RENAME, + TYPE, + UNKNOWN, + UNMERGED +} + +export function fromStatus(status: string): GitChangeType { + switch (status) { + case 'A': return GitChangeType.ADD; + case 'C': return GitChangeType.COPY; + case 'D': return GitChangeType.DELETE; + case 'M': return GitChangeType.MODIFY; + case 'R': return GitChangeType.RENAME; + case 'T': return GitChangeType.TYPE; + case 'X': return GitChangeType.UNKNOWN; + case 'U': return GitChangeType.UNMERGED; + } + + if (status.match(/R[0-9]+/)) { return GitChangeType.RENAME; } + if (status.match(/C[0-9]+/)) { return GitChangeType.COPY; } + + return GitChangeType.MODIFY; +} + +export class SlimFileChange { + public originalContent: string; + public content: string; + + constructor( + public readonly filePath: string, + public readonly originalFilePath: string, + public readonly status: GitChangeType, + public readonly fileName: string + ) { } +} + +export class RichFileChange { + constructor( + public readonly filePath: string, + public readonly originalFilePath: string, + public readonly status: GitChangeType, + public readonly fileName: string + ) { } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/models/remote.ts b/extensions/git-extended/src/common/models/remote.ts new file mode 100644 index 00000000000..5b21e704485 --- /dev/null +++ b/extensions/git-extended/src/common/models/remote.ts @@ -0,0 +1,21 @@ +export interface IRemote { + readonly name: string; + readonly url: string; +} + +export interface IGitRemoteURL { + /** The hostname of the remote. */ + readonly hostname: string; + + /** + * The owner of the GitHub repository. This will be null if the URL doesn't + * take the form of a GitHub repository URL (e.g., owner/name). + */ + readonly owner: string | null; + + /** + * The name of the GitHub repository. This will be null if the URL doesn't + * take the form of a GitHub repository URL (e.g., owner/name). + */ + readonly name: string | null; +} diff --git a/extensions/git-extended/src/common/models/repository.ts b/extensions/git-extended/src/common/models/repository.ts new file mode 100644 index 00000000000..e50b1feadd1 --- /dev/null +++ b/extensions/git-extended/src/common/models/repository.ts @@ -0,0 +1,19 @@ +import { IGitRemoteURL } from './remote'; + +export class GitHubRepository { + constructor( + public name: string, + public owner: string, + public url: string + ) { } +} +export class Repository { + public path: string; + + public remotes: IGitRemoteURL[]; + + constructor(path: string, remotes: IGitRemoteURL[]) { + this.path = path; + this.remotes = remotes; + } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/remote.ts b/extensions/git-extended/src/common/remote.ts new file mode 100644 index 00000000000..eb17acfde14 --- /dev/null +++ b/extensions/git-extended/src/common/remote.ts @@ -0,0 +1,47 @@ +import { GitProcess } from 'dugite'; +import { IGitRemoteURL } from './models/remote'; + +export async function getRemotes( + path: string +) { + const result = await GitProcess.exec(['remote', '-v'], path); + const output = result.stdout; + const lines = output.split('\n'); + const remotes = lines + .filter(x => x.endsWith('(fetch)')) + .map(x => x.split(/\s+/)) + .map(x => ({ name: x[0], url: x[1] })); + + return remotes; +} + +/** Parse the remote information from URL. */ +export function parseRemote(url: string): IGitRemoteURL | null { + // Examples: + // https://github.com/octocat/Hello-World.git + // https://github.com/octocat/Hello-World.git/ + // git@github.com:octocat/Hello-World.git + // git:github.com/octocat/Hello-World.git + const regexes = [ + new RegExp('^https?://(?:.+@)?(.+)/(.+)/(.+?)(?:/|.git/?)?$'), + new RegExp('^git@(.+):(.+)/(.+?)(?:/|.git)?$'), + new RegExp('^git:(.+)/(.+)/(.+?)(?:/|.git)?$'), + new RegExp('^ssh://git@(.+)/(.+)/(.+?)(?:/|.git)?$') + ]; + + for (const regex of regexes) { + const result = url.match(regex); + if (!result) { + continue; + } + + const hostname = result[1]; + const owner = result[2]; + const name = result[3]; + if (hostname) { + return { hostname, owner, name }; + } + } + + return null; +} diff --git a/extensions/git-extended/src/extension.ts b/extensions/git-extended/src/extension.ts new file mode 100644 index 00000000000..4ba37bb9077 --- /dev/null +++ b/extensions/git-extended/src/extension.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as vscode from 'vscode'; +import { CommitsProvider } from './commitsProvider'; +import { PRProvider } from './prProvider'; +import { Repository } from './common/models/repository'; +import { getRemotes, parseRemote } from './common/remote'; + +export async function activate(context: vscode.ExtensionContext) { + const rootPath = vscode.workspace.rootPath; + const remotes = await getRemotes(rootPath); + const remoteUrls = remotes.map(remote => parseRemote(remote.url)); + const repository = new Repository(rootPath, remoteUrls); + new CommitsProvider().activate(context, rootPath, repository); + new PRProvider().activate(context, rootPath, repository); +} \ No newline at end of file diff --git a/extensions/git-extended/src/prProvider.ts b/extensions/git-extended/src/prProvider.ts new file mode 100644 index 00000000000..4be162bd475 --- /dev/null +++ b/extensions/git-extended/src/prProvider.ts @@ -0,0 +1,242 @@ +import * as vscode from 'vscode'; +// import { Octokat } from 'octokat'; +var Octokat = require('octokat'); +// import * as https from 'https'; +// import * as fs from 'fs'; +// import * as tmp from 'tmp'; +import * as path from 'path'; +import * as request from 'request'; + +import { FileChangeItem } from './commitsProvider'; +import { parseDiff, DIFF_HUNK_INFO } from './common/diff'; +import { GitChangeType } from './common/models/file'; +import { Repository } from './common//models/repository'; +import { Comment } from './common/models/comment'; +import * as _ from 'lodash'; +export class PullRequest { + constructor(public octoPRItem: any) { }; +} + +export class PRProvider implements vscode.TreeDataProvider, vscode.CommentProvider { + private _fileChanges: FileChangeItem[]; + private _comments?: Comment[]; + private context: vscode.ExtensionContext; + private workspaceRoot: string; + private repository: Repository; + private octo: any; + private icons: any; + + constructor() { + this.octo = new Octokat(); + } + + activate(context: vscode.ExtensionContext, workspaceRoot: string, repository: Repository) { + this.context = context; + this.workspaceRoot = workspaceRoot; + this.repository = repository; + + vscode.window.registerTreeDataProvider('pr', this); + this.icons = { + light: { + Modified: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-modified.svg')), + Added: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-added.svg')), + Deleted: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-deleted.svg')), + Renamed: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-renamed.svg')), + Copied: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-copied.svg')), + Untracked: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-untrackedt.svg')), + Ignored: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-ignored.svg')), + Conflict: context.asAbsolutePath(path.join('resources', 'icons', 'light', 'status-conflict.svg')), + }, + dark: { + Modified: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-modified.svg')), + Added: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-added.svg')), + Deleted: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-deleted.svg')), + Renamed: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-renamed.svg')), + Copied: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-copied.svg')), + Untracked: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-untracked.svg')), + Ignored: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-ignored.svg')), + Conflict: context.asAbsolutePath(path.join('resources', 'icons', 'dark', 'status-conflict.svg')) + } + }; + + vscode.workspace.registerCommentProvider(this); + } + + getTreeItem(element: PullRequest | FileChangeItem): vscode.TreeItem { + + if (element instanceof PullRequest) { + return { + label: element.octoPRItem.title, + collapsibleState: 1 + }; + } else { + let iconUri: string; + let iconDarkUri: string; + + switch (element.status) { + case GitChangeType.ADD: + iconUri = this.icons.light.Added; + iconDarkUri = this.icons.dark.Added; + break; + case GitChangeType.COPY: + iconUri = this.icons.light.Copied; + iconDarkUri = this.icons.dark.Copied; + break; + case GitChangeType.DELETE: + iconUri = this.icons.light.Deleted; + iconDarkUri = this.icons.dark.Deleted; + break; + case GitChangeType.MODIFY: + iconUri = this.icons.light.Modified; + iconDarkUri = this.icons.dark.Modified; + break; + case GitChangeType.RENAME: + iconUri = this.icons.light.Renamed; + iconDarkUri = this.icons.dark.Renamed; + break; + } + element.iconPath = { + light: iconUri, + dark: iconDarkUri + }; + return element; + } + } + + getChildren(element?: PullRequest): PullRequest[] | Thenable | FileChangeItem[] | Thenable { + if (element) { + return new Promise((resolve, rxeject) => { + request({ + followAllRedirects: true, + url: element.octoPRItem.diffUrl + }, async (error, response, body) => { + // map comments to FileChangeItem + // registerCommentProvider + const rawComments = await element.octoPRItem.reviewComments.fetch(); + const comments: Comment[] = parseComments(rawComments.items); + let richContentChanges = await parseDiff(body, this.repository, element.octoPRItem.base.sha); + let fileChanges = richContentChanges.map(change => { + let changedItem = new FileChangeItem(change.fileName ? change.fileName : change.filePath, change.status, this.context, change.fileName, this.workspaceRoot); + changedItem.filePath = change.filePath; + changedItem.parentFilePath = change.originalFilePath; + changedItem.populateCommandArgs(); + return changedItem; + }); + this._fileChanges = fileChanges; + this._comments = comments; + + vscode.workspace.onDidOpenTextDocument(e => { + let matchingComments = getMatchingCommentsForDiffViewEditor(e.fileName, fileChanges, comments); + console.log('---diff editor---') + for (let i = 0; i < matchingComments.length; i++) { + let cm = matchingComments[i]; + console.log(`after line: ${cm.diff_hunk_range.start + cm.position - 1 - 1}, ${'@' + cm.user.login}: '${cm.body}'`); + } + + matchingComments = getMatchingCommentsForNormalEditor(e.fileName, this.workspaceRoot, comments); + console.log('---editor---') + for (let i = 0; i < matchingComments.length; i++) { + let cm = matchingComments[i]; + console.log(`after line: ${cm.diff_hunk_range.start + cm.position - 1 - 1}, ${'@' + cm.user.login}: '${cm.body}'`); + } + }); + + resolve(fileChanges); + }); + }); + } else { + if (this.repository.remotes && this.repository.remotes.length > 0) { + let promises = this.repository.remotes.map(remote => this.octo.repos(remote.owner, remote.name).pulls.fetch().then(prs => { + return prs.items.map(item => new PullRequest(item)); + })); + return Promise.all(promises).then(values => { + let prs = []; + values.forEach(value => { + prs.push(...value); + }); + + return prs; + }); + } + + return []; + } + } + + async provideComments(document: vscode.TextDocument): Promise { + if (!this._comments) { + return []; + } + + let matchingComments = getMatchingCommentsForDiffViewEditor(document.fileName, this._fileChanges, this._comments); + if (!matchingComments || !matchingComments.length) { + matchingComments = getMatchingCommentsForNormalEditor(document.fileName, this.workspaceRoot, this._comments); + } + + if (!matchingComments || !matchingComments.length) { + return []; + } + + let sections = _.groupBy(matchingComments, comment => comment.position); + let ret = []; + + for (let i in sections) { + let comments = sections[i]; + + const comment = comments[0]; + const pos = new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0); + const range = new vscode.Range(pos, pos); + + ret.push({ + range, + comments: comments.map(comment => { + return { + body: new vscode.MarkdownString(comment.body), + userName: comment.user.login + }; + }) + }); + } + + return ret; + } +} + +function parseComments(comments: any[]): Comment[] { + for (let i = 0; i < comments.length; i++) { + let diff_hunk = comments[i].diffHunk; + let hunk_info = DIFF_HUNK_INFO.exec(diff_hunk); + let oriStartLine = Number(hunk_info[1]); + let oriLen = Number(hunk_info[3]) | 0; + let startLine = Number(hunk_info[5]); + let len = Number(hunk_info[7]) | 0; + comments[i].diff_hunk_range = { + originalStart: oriStartLine, + originalLength: oriLen, + start: startLine, + length: len + }; + } + + return comments; +} + +function getMatchingCommentsForDiffViewEditor(filePath: string, items: FileChangeItem[], comments: Comment[]): Comment[] { + let fileChangeItem = items.filter(item => filePath === path.resolve(item.workspaceRoot, item.filePath)); + + if (fileChangeItem.length === 0) { + return []; + } else { + let fileName = fileChangeItem[0].fileName; + let matchingComments = comments.filter(comment => comment.path === fileName); + + return matchingComments; + } +} + +function getMatchingCommentsForNormalEditor(filePath: string, workspaceRoot: string, comments: Comment[]): Comment[] { + // @todo, we should check commit id + let matchingComments = comments.filter(comment => path.resolve(workspaceRoot, comment.path) === filePath); + return matchingComments; +} + diff --git a/extensions/git-extended/src/typings/ref.d.ts b/extensions/git-extended/src/typings/ref.d.ts new file mode 100644 index 00000000000..c9849d48e08 --- /dev/null +++ b/extensions/git-extended/src/typings/ref.d.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// diff --git a/extensions/git-extended/tsconfig.json b/extensions/git-extended/tsconfig.json new file mode 100644 index 00000000000..9508ff037b1 --- /dev/null +++ b/extensions/git-extended/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "noUnusedLocals": true, + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "./src", + "jsx": "react" + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/extensions/git-extended/yarn.lock b/extensions/git-extended/yarn.lock new file mode 100644 index 00000000000..fbf82ecae16 --- /dev/null +++ b/extensions/git-extended/yarn.lock @@ -0,0 +1,482 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "9.6.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.2.tgz#e49ac1adb458835e95ca6487bc20f916b37aff23" + +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +checksum@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/checksum/-/checksum-0.1.1.tgz#dc6527d4c90be8560dbd1ed4cecf3297d528e9e9" + dependencies: + optimist "~0.3.5" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +combined-stream@1.0.6, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +dugite@^1.28.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.61.0.tgz#d5dad047af478c1312f8b791eb56d9935e6fc47d" + dependencies: + checksum "^0.1.1" + mkdirp "^0.5.1" + progress "^2.0.0" + request "^2.83.0" + rimraf "^2.5.4" + tar "^4.0.2" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lodash@4.17.5, lodash@^4.16.4: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@^2.1.12, mime-types@~2.1.17: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minipass@^2.2.1, minipass@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40" + dependencies: + safe-buffer "^5.1.1" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +octokat@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/octokat/-/octokat-0.8.0.tgz#50841ca255743f91a715d11a1bde76f2eefcd97a" + dependencies: + lodash "^4.16.4" + xmlhttprequest "~1.8.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +optimist@~0.3.5: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + dependencies: + wordwrap "~0.0.2" + +os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +request@^2.81.0, request@^2.83.0: + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +rimraf@^2.5.4: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +sshpk@^1.7.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +tar@^4.0.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.2.4" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.1" + yallist "^3.0.2" + +tmp@^0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typescript@^2.1.4: + version "2.8.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624" + +uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xmlhttprequest@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 64bac6dd228..c77b0a7137b 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -936,6 +936,22 @@ export interface Command { tooltip?: string; arguments?: any[]; } + + +export interface CommentThread { + readonly range: IRange; + readonly comments: Comment[]; +} + +export interface Comment { + readonly body: IMarkdownString; + readonly userName: string; +} + +export interface CommentProvider { + provideComments(model: model.ITextModel, token: CancellationToken): CommentThread[]; +} + export interface ICodeLensSymbol { range: IRange; id?: string; diff --git a/src/vs/editor/contrib/review/review.ts b/src/vs/editor/contrib/review/review.ts index 1c410273003..5c0332d869f 100644 --- a/src/vs/editor/contrib/review/review.ts +++ b/src/vs/editor/contrib/review/review.ts @@ -5,6 +5,7 @@ 'use strict'; import 'vs/css!./review'; +import * as modes from 'vs/editor/common/modes'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IViewZone } from 'vs/editor/browser/editorBrowser'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -13,10 +14,8 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { ZoneWidget, IOptions } from '../zoneWidget/zoneWidget'; -import { MarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; -import { IComment, getComments } from 'vs/editor/contrib/review/reviewProvider'; -import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -67,7 +66,7 @@ export class ReviewZoneWidget extends ZoneWidget { container.appendChild(this._domNode); } - display(comments: IComment[], lineNumber: number) { + display(comments: modes.Comment[], lineNumber: number) { this.show({ lineNumber: lineNumber, column: 1 }, 2); for (let i = 0; i < comments.length; i++) { @@ -76,21 +75,21 @@ export class ReviewZoneWidget extends ZoneWidget { let header = document.createElement('h4'); let author = document.createElement('strong'); author.className = 'author'; - author.innerText = comments[i].user; - let time = document.createElement('span'); - time.className = 'created_at'; - time.innerText = comments[i].created_at; + author.innerText = comments[i].userName; + // let time = document.createElement('span'); + // time.className = 'created_at'; + // time.innerText = comments[i].created_at; header.appendChild(author); - header.appendChild(time); + // header.appendChild(time); singleCommentContainer.appendChild(header); let body = document.createElement('div'); body.className = 'comment-body'; singleCommentContainer.appendChild(body); - let md = new MarkdownString(comments[i].body); + let md = comments[i].body; body.appendChild(renderMarkdown(md)); this._domNode.appendChild(singleCommentContainer); - // this._domNode.appendChild(document.createElement('textarea')); } + // this._domNode.appendChild(document.createElement('textarea')); this._resizeObserver = new ResizeObserver(entries => { if (entries[0].target === this._domNode) { const lineHeight = this.editor.getConfiguration().lineHeight; @@ -111,7 +110,6 @@ export class ReviewZoneWidget extends ZoneWidget { } export class ReviewController implements IEditorContribution { - private globalToDispose: IDisposable[]; private localToDispose: IDisposable[]; private editor: ICodeEditor; @@ -119,16 +117,18 @@ export class ReviewController implements IEditorContribution { private _domNode: HTMLElement; private _zoneWidget: ReviewZoneWidget; private _reviewPanelVisible: IContextKey; + private _commentThreads: modes.CommentThread[]; constructor( editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService ) { this.editor = editor; this.globalToDispose = []; this.localToDispose = []; this.decorationIDs = []; this.mouseDownInfo = null; + this._commentThreads = []; this._reviewPanelVisible = ctxReviewPanelVisible.bindTo(contextKeyService); this._domNode = document.createElement('div'); @@ -161,20 +161,6 @@ export class ReviewController implements IEditorContribution { this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); - - this.editor.changeDecorations(accessor => { - this.decorationIDs = accessor.deltaDecorations(this.decorationIDs, [ - { - range: { - startLineNumber: 6, - startColumn: 1, - endLineNumber: 6, - endColumn: 1 - }, - options: REVIEWL_DECORATION - } - ]); - }); } private mouseDownInfo: { lineNumber: number, iconClicked: boolean }; @@ -232,9 +218,30 @@ export class ReviewController implements IEditorContribution { this._reviewPanelVisible.set(true); this._zoneWidget = new ReviewZoneWidget(this.editor); - this._zoneWidget.display(getComments(), lineNumber); + this._zoneWidget.display(this.getComments(lineNumber), lineNumber); } + getComments(line: number): modes.Comment[] { + for (let i = 0; i < this._commentThreads.length; i++) { + if (this._commentThreads[i].range.startLineNumber === line) { + return this._commentThreads[i].comments; + } + } + + return []; + } + + setComments(commentThreads: modes.CommentThread[]): void { + this._commentThreads = commentThreads; + this.editor.changeDecorations(accessor => { + this.decorationIDs = accessor.deltaDecorations(this.decorationIDs, commentThreads.map(thread => ({ + range: thread.range, + options: REVIEWL_DECORATION + }))); + }); + } + + public closeWidget(): void { this._reviewPanelVisible.reset(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index dd91585c691..2e8a7e01f79 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4998,6 +4998,20 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface CommentThread { + readonly range: IRange; + readonly comments: Comment[]; + } + + export interface Comment { + readonly body: IMarkdownString; + readonly userName: string; + } + + export interface CommentProvider { + provideComments(model: editor.ITextModel, token: CancellationToken): CommentThread[]; + } + export interface ICodeLensSymbol { range: IRange; id?: string; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5d0f2e25d86..d2429fec428 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -723,4 +723,26 @@ declare module 'vscode' { } //#endregion + + interface CommentThread { + range: Range; + comments: Comment[]; + } + + interface Comment { + body: MarkdownString; + userName: string; + } + + /** + * TODO: force update event? + * TODO: resolve step? + */ + interface CommentProvider { + provideComments(document: TextDocument, token: CancellationToken): Promise; + } + + namespace workspace { + export function registerCommentProvider(provider: CommentProvider): Disposable; + } } diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index 3418ddaf1a2..2949908a2f5 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -48,6 +48,7 @@ import './mainThreadTerminalService'; import './mainThreadTreeViews'; import './mainThreadLogService'; import './mainThreadWebview'; +import './mainThreadComments'; import './mainThreadWindow'; import './mainThreadWorkspace'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts new file mode 100644 index 00000000000..ada23f39c96 --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ITextModel } from 'vs/editor/common/model'; +import * as modes from 'vs/editor/common/modes'; +import { ReviewController } from 'vs/editor/contrib/review/review'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { keys } from '../../../base/common/map'; +import { IWorkbenchEditorService } from '../../services/editor/common/editorService'; +import { ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape } from '../node/extHost.protocol'; + +@extHostNamedCustomer(MainContext.MainThreadComments) +export class MainThreadComments extends Disposable implements MainThreadCommentsShape { + + private _proxy: ExtHostCommentsShape; + private _providers = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IEditorGroupService editorGroupService: IEditorGroupService, + @IWorkbenchEditorService workbenchEditorService: IWorkbenchEditorService, + @ICodeEditorService private _codeEditorService: ICodeEditorService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); + editorGroupService.onEditorsChanged(e => { + const outerEditor = this.getFocusedEditor(); + if (!outerEditor) { + return; + } + + const controller = ReviewController.get(outerEditor); + if (!controller) { + return; + } + + this.provideComments(outerEditor.getModel()).then(commentThreads => { + controller.setComments(commentThreads); + }); + }); + } + + $registerCommentProvider(handle: number): void { + this._providers.set(handle, undefined); + } + + $unregisterCommentProvider(handle: number): void { + throw new Error('Method not implemented.'); + } + + dispose(): void { + throw new Error('Method not implemented.'); + } + + getFocusedEditor(): ICodeEditor { + let editor = this._codeEditorService.getFocusedCodeEditor(); + // if\ + + return editor; + } + + async provideComments(model: ITextModel): Promise { + const result: modes.CommentThread[] = []; + for (const handle of keys(this._providers)) { + result.push(...await this._proxy.$providerComments(handle, model.uri)); + } + return result; + } + +} diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 82fc8af5fea..2b7349f2a93 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -58,6 +58,7 @@ import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { OverviewRulerLane } from 'vs/editor/common/model'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; +import { ExtHostComments } from './extHostComments'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -119,6 +120,7 @@ export function createApiFactory( const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); + const exthostCommentProviders = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostDocuments)); // Check that no named customers are missing const expected: ProxyIdentifier[] = Object.keys(ExtHostContext).map((key) => ExtHostContext[key]); @@ -540,6 +542,9 @@ export function createApiFactory( }), registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => { return extHostFileSystem.registerSearchProvider(scheme, provider); + }), + registerCommentProvider: proposedApiFunction(extension, (provider: vscode.CommentProvider) => { + return exthostCommentProviders.registerCommentProvider(provider); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 8a1b10bc57b..d45d9f01b71 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -104,6 +104,11 @@ export interface MainThreadCommandsShape extends IDisposable { $getCommands(): Thenable; } +export interface MainThreadCommentsShape extends IDisposable { + $registerCommentProvider(handle: number): void; + $unregisterCommentProvider(handle: number): void; +} + export interface MainThreadConfigurationShape extends IDisposable { $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any, resource: UriComponents): TPromise; $removeConfigurationOption(target: ConfigurationTarget, key: string, resource: UriComponents): TPromise; @@ -813,10 +818,15 @@ export interface ExtHostProgressShape { $acceptProgressCanceled(handle: number): void; } +export interface ExtHostCommentsShape { + $providerComments(handle: number, document: UriComponents): TPromise; +} + // --- proxy identifiers export const MainContext = { MainThreadCommands: >createMainId('MainThreadCommands'), + MainThreadComments: createMainId('MainThreadComments'), MainThreadConfiguration: createMainId('MainThreadConfiguration'), MainThreadDebugService: createMainId('MainThreadDebugService'), MainThreadDecorations: createMainId('MainThreadDecorations'), @@ -871,5 +881,6 @@ export const ExtHostContext = { ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), - ExtHostProgress: createMainId('ExtHostProgress') + ExtHostProgress: createMainId('ExtHostProgress'), + ExtHostComments: createMainId('ExtHostComments') }; diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts new file mode 100644 index 00000000000..4ebd9ffd66b --- /dev/null +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- +* 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 { asWinJsPromise } from 'vs/base/common/async'; +import { values } from 'vs/base/common/map'; +import URI, { UriComponents } from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as modes from 'vs/editor/common/modes'; +import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; +import * as vscode from 'vscode'; +import { flatten } from '../../../base/common/arrays'; +import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol'; + +import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverters'; + + +export class ExtHostComments implements ExtHostCommentsShape { + private static handlePool = 0; + + private _proxy: MainThreadCommentsShape; + + private _providers = new Map(); + + constructor( + mainContext: IMainContext, + private readonly _documents: ExtHostDocuments, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadComments); + } + + registerCommentProvider( + provider: vscode.CommentProvider + ): vscode.Disposable { + const handle = ExtHostComments.handlePool++; + this._providers.set(handle, provider); + + this._proxy.$registerCommentProvider(handle); + return { + dispose: () => { + this._proxy.$unregisterCommentProvider(handle); + this._providers.delete(handle); + } + }; + } + + $providerComments(handle: number, uri: UriComponents): TPromise { + const data = this._documents.getDocumentData(URI.revive(uri)); + if (!data || !data.document) { + return TPromise.as([]); + } + + return asWinJsPromise(token => { + const allProviderResults = values(this._providers).map(provider => provider.provideComments(data.document, token)); + return TPromise.join(allProviderResults); + }) + .then(flatten) + .then(comments => comments.map(convertCommentThread)); + } +} + +function convertCommentThread(vscodeCommentThread: vscode.CommentThread): modes.CommentThread { + return { + range: extHostTypeConverter.fromRange(vscodeCommentThread.range), + comments: vscodeCommentThread.comments.map(convertComment) + }; +} + +function convertComment(vscodeComment: vscode.Comment): modes.Comment { + return { + body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), + userName: vscodeComment.userName + }; +}