diff --git a/package.json b/package.json index fcd1700..dd18e8b 100644 --- a/package.json +++ b/package.json @@ -88,12 +88,12 @@ "discord.detailsEditing": { "type": "string", "default": "Editing {filename}", - "description": "Custom string for the details section of the rich presence\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size." + "description": "Custom string for the details section of the rich presence\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size.\n\t- '{gitreponame}' will be replaced with the active Git repository name (from the git URL)\n\t- '{gitbranch}' will be replaced with the current active branch name." }, "discord.detailsDebugging": { "type": "string", "default": "Debugging {filename}", - "description": "Custom string for the details section of the rich presence when debugging\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size." + "description": "Custom string for the details section of the rich presence when debugging\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size.\n\t- '{gitreponame}' will be replaced with the active Git repository name (from the git URL)\n\t- '{gitbranch}' will be replaced with the current active branch name." }, "discord.detailsIdle": { "type": "string", @@ -103,12 +103,12 @@ "discord.lowerDetailsEditing": { "type": "string", "default": "Workspace: {workspace}", - "description": "Custom string for the state section of the rich presence\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size." + "description": "Custom string for the state section of the rich presence\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name.\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size.\n\t- '{gitreponame}' will be replaced with the active Git repository name (from the git URL)\n\t- '{gitbranch}' will be replaced with the current active branch name." }, "discord.lowerDetailsDebugging": { "type": "string", "default": "Debugging: {workspace}", - "description": "Custom string for the state section of the rich presence when debugging\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name..\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size." + "description": "Custom string for the state section of the rich presence when debugging\n\t- '{null}' will be replaced with an empty space.\n\t- '{filename}' will be replaced with the current file name.\n\t- '{dirname}' will get replaced with the folder name that has the current file.\n\t- '{fulldirname}' will get replaced with the full directory name without the current file name..\n\t- '{workspace}' will be replaced with the current workspace name, if any.\n\t- '{currentcolumn}' will get replaced with the current column of the current line.\n\t- '{currentline}' will get replaced with the current line number.\n\t- '{totallines}' will get replaced with the total line number.\n\t- '{filesize}' will get replaced with the current file's size.\n\t- '{gitreponame}' will be replaced with the active Git repository name (from the git URL)\n\t- '{gitbranch}' will be replaced with the current active branch name." }, "discord.lowerDetailsIdle": { "type": "string", diff --git a/src/client/RPCClient.ts b/src/client/RPCClient.ts index f5fbf6c..c4564ce 100644 --- a/src/client/RPCClient.ts +++ b/src/client/RPCClient.ts @@ -3,6 +3,7 @@ import { Disposable, StatusBarItem, Uri, window, workspace } from 'vscode'; import * as vsls from 'vsls'; import Activity from '../structures/Activity'; import Logger from '../structures/Logger'; +import { API } from '../git'; const clipboardy = require('clipboardy'); // eslint-disable-line let activityTimer: NodeJS.Timer; @@ -12,9 +13,11 @@ export default class RPCClient implements Disposable { public config = workspace.getConfiguration('discord'); + public git!: API; + private _rpc: any; - private readonly _activity = new Activity(); + private readonly _activity = new Activity(this); private readonly _clientId: string; @@ -74,7 +77,7 @@ export default class RPCClient implements Disposable { setTimeout(() => (this.statusBarIcon.text = '$(globe)'), 5000); if (activityTimer) clearInterval(activityTimer); - await this.setActivity(); + await this.setActivity(this.config.get('workspaceElapsedTime')); activityTimer = setInterval(async () => { this.config = workspace.getConfiguration('discord'); diff --git a/src/extension.ts b/src/extension.ts index d4e51c5..8067514 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,8 +1,18 @@ -import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode'; +import { + commands, + ExtensionContext, + StatusBarAlignment, + StatusBarItem, + window, + workspace, + extensions +} from 'vscode'; import RPCClient from './client/RPCClient'; import Logger from './structures/Logger'; +import { GitExtension } from './git'; const { register } = require('discord-rpc'); // eslint-disable-line +const sleep = (wait: number) => new Promise(resolve => setTimeout(resolve, wait)); let loginTimeout: NodeJS.Timer; const statusBarIcon: StatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); @@ -13,6 +23,18 @@ register(config.get('clientID')); const rpc = new RPCClient(config.get('clientID')!, statusBarIcon); export async function activate(context: ExtensionContext) { + try { + const ext = extensions.getExtension('vscode.git')!; + await ext.activate(); + rpc.git = ext.exports.getAPI(1); + } catch { + // We loaded before the git extension, give it a bit to load + // In a perfect world this shouldn't happen + await sleep(2000); + const ext = extensions.getExtension('vscode.git')!; + await ext.activate(); + rpc.git = ext.exports.getAPI(1); + } Logger.log('Discord Presence activated!'); let isWorkspaceExcluded = false; diff --git a/src/git.d.ts b/src/git.d.ts new file mode 100644 index 0000000..90e611e --- /dev/null +++ b/src/git.d.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, SourceControlInputBox, Event, CancellationToken } from 'vscode'; + +export interface Git { + readonly path: string; +} + +export interface InputBox { + value: string; +} + +export const enum RefType { + Head, + RemoteHead, + Tag +} + +export interface Ref { + readonly type: RefType; + readonly name?: string; + readonly commit?: string; + readonly remote?: string; +} + +export interface UpstreamRef { + readonly remote: string; + readonly name: string; +} + +export interface Branch extends Ref { + readonly upstream?: UpstreamRef; + readonly ahead?: number; + readonly behind?: number; +} + +export interface Commit { + readonly hash: string; + readonly message: string; + readonly parents: string[]; +} + +export interface Submodule { + readonly name: string; + readonly path: string; + readonly url: string; +} + +export interface Remote { + readonly name: string; + readonly fetchUrl?: string; + readonly pushUrl?: string; + readonly isReadOnly: boolean; +} + +export const enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED +} + +export interface Change { + /** + * Returns either `originalUri` or `renameUri`, depending + * on whether this change is a rename change. When + * in doubt always use `uri` over the other two alternatives. + */ + readonly uri: Uri; + readonly originalUri: Uri; + readonly renameUri: Uri | undefined; + readonly status: Status; +} + +export interface RepositoryState { + readonly HEAD: Branch | undefined; + readonly refs: Ref[]; + readonly remotes: Remote[]; + readonly submodules: Submodule[]; + readonly rebaseCommit: Commit | undefined; + + readonly mergeChanges: Change[]; + readonly indexChanges: Change[]; + readonly workingTreeChanges: Change[]; + + readonly onDidChange: Event; +} + +export interface RepositoryUIState { + readonly selected: boolean; + readonly onDidChange: Event; +} + +export interface Repository { + readonly rootUri: Uri; + readonly inputBox: InputBox; + readonly state: RepositoryState; + readonly ui: RepositoryUIState; + + getConfigs(): Promise<{ key: string; value: string; }[]>; + getConfig(key: string): Promise; + setConfig(key: string, value: string): Promise; + + getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; + detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>; + buffer(ref: string, path: string): Promise; + show(ref: string, path: string): Promise; + getCommit(ref: string): Promise; + + clean(paths: string[]): Promise; + + apply(patch: string, reverse?: boolean): Promise; + diff(cached?: boolean): Promise; + diffWithHEAD(path: string): Promise; + diffWith(ref: string, path: string): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffBlobs(object1: string, object2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + + hashObject(data: string): Promise; + + createBranch(name: string, checkout: boolean, ref?: string): Promise; + deleteBranch(name: string, force?: boolean): Promise; + getBranch(name: string): Promise; + setBranchUpstream(name: string, upstream: string): Promise; + + getMergeBase(ref1: string, ref2: string): Promise; + + status(): Promise; + checkout(treeish: string): Promise; + + addRemote(name: string, url: string): Promise; + removeRemote(name: string): Promise; + + fetch(remote?: string, ref?: string): Promise; + pull(): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise; +} + +export interface API { + readonly git: Git; + readonly repositories: Repository[]; + readonly onDidOpenRepository: Event; + readonly onDidCloseRepository: Event; +} + +export interface GitExtension { + readonly enabled: boolean; + readonly onDidChangeEnablement: Event; + + /** + * Returns a specific API version. + * + * Throws error if git extension is disabled. You can listed to the + * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event + * to know when the extension becomes enabled/disabled. + * + * @param version Version number. + * @returns API instance + */ + getAPI(version: 1): API; +} + +export const enum GitErrorCodes { + BadConfigFile = 'BadConfigFile', + AuthenticationFailed = 'AuthenticationFailed', + NoUserNameConfigured = 'NoUserNameConfigured', + NoUserEmailConfigured = 'NoUserEmailConfigured', + NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified', + NotAGitRepository = 'NotAGitRepository', + NotAtRepositoryRoot = 'NotAtRepositoryRoot', + Conflict = 'Conflict', + StashConflict = 'StashConflict', + UnmergedChanges = 'UnmergedChanges', + PushRejected = 'PushRejected', + RemoteConnectionError = 'RemoteConnectionError', + DirtyWorkTree = 'DirtyWorkTree', + CantOpenResource = 'CantOpenResource', + GitNotFound = 'GitNotFound', + CantCreatePipe = 'CantCreatePipe', + CantAccessRemote = 'CantAccessRemote', + RepositoryNotFound = 'RepositoryNotFound', + RepositoryIsLocked = 'RepositoryIsLocked', + BranchNotFullyMerged = 'BranchNotFullyMerged', + NoRemoteReference = 'NoRemoteReference', + InvalidBranchName = 'InvalidBranchName', + BranchAlreadyExists = 'BranchAlreadyExists', + NoLocalChanges = 'NoLocalChanges', + NoStashFound = 'NoStashFound', + LocalChangesOverwritten = 'LocalChangesOverwritten', + NoUpstreamBranch = 'NoUpstreamBranch', + IsInSubmodule = 'IsInSubmodule', + WrongCase = 'WrongCase', + CantLockRef = 'CantLockRef', + CantRebaseMultipleBranches = 'CantRebaseMultipleBranches', +} diff --git a/src/structures/Activity.ts b/src/structures/Activity.ts index c792321..b1fa6e3 100644 --- a/src/structures/Activity.ts +++ b/src/structures/Activity.ts @@ -1,6 +1,7 @@ import { basename, parse, sep } from 'path'; import { debug, Disposable, env, window, workspace } from 'vscode'; import * as vsls from 'vsls'; +import RPCClient from '../client/RPCClient'; const lang = require('../data/languages.json'); // eslint-disable-line const knownExtentions: { [key: string]: { image: string } } = lang.knownExtentions; @@ -31,14 +32,16 @@ interface FileDetail { totalLines?: string; currentLine?: string; currentColumn?: string; + gitbranch?: string; + gitreponame?: string; } export default class Activity implements Disposable { private _state: State | null = null; - private readonly _config = workspace.getConfiguration('discord'); + private _lastKnownFile: string = ''; - private _lastKnownFile = ''; + public constructor(public client: RPCClient) {} public get state() { return this._state; @@ -107,7 +110,7 @@ export default class Activity implements Disposable { ), largeImageKey: largeImageKey ? largeImageKey.image || largeImageKey : 'txt', largeImageText: window.activeTextEditor - ? this._config + ? this.client.config .get('largeImage')! .replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt') .replace( @@ -118,13 +121,13 @@ export default class Activity implements Disposable { ) .replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT') || window.activeTextEditor.document.languageId.padEnd(2, '\u200b') - : this._config.get('largeImageIdle'), + : this.client.config.get('largeImageIdle'), smallImageKey: debug.activeDebugSession ? 'debug' : env.appName.includes('Insiders') ? 'vscode-insiders' : 'vscode', - smallImageText: this._config.get('smallImage')!.replace('{appname}', env.appName), + smallImageText: this.client.config.get('smallImage')!.replace('{appname}', env.appName), }; return this._state; @@ -237,7 +240,7 @@ export default class Activity implements Disposable { } private async _generateDetails(debugging: string, editing: string, idling: string, largeImageKey: any) { - let raw: string = this._config.get(idling)!.replace('{null}', empty); + let raw: string = this.client.config.get(idling)!.replace('{null}', empty); let filename = null; let dirname = null; let checkState = false; @@ -262,12 +265,12 @@ export default class Activity implements Disposable { } if (debug.activeDebugSession) { - raw = this._config.get(debugging)!; + raw = this.client.config.get(debugging)!; } else { - raw = this._config.get(editing)!; + raw = this.client.config.get(editing)!; } - const { totalLines, size, currentLine, currentColumn } = await this._generateFileDetails(raw); + const { totalLines, size, currentLine, currentColumn, gitbranch, gitreponame } = await this._generateFileDetails(raw); raw = raw .replace('{null}', empty) .replace('{filename}', filename) @@ -277,7 +280,7 @@ export default class Activity implements Disposable { '{workspace}', checkState && workspaceFolder ? workspaceFolder.name - : this._config.get('lowerDetailsNotFound')!.replace('{null}', empty), + : this.client.config.get('lowerDetailsNotFound')!.replace('{null}', empty), ) .replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt') .replace( @@ -291,6 +294,8 @@ export default class Activity implements Disposable { if (size) raw = raw.replace('{filesize}', size); if (currentLine) raw = raw.replace('{currentline}', currentLine); if (currentColumn) raw = raw.replace('{currentcolumn}', currentColumn); + if (gitbranch) raw = raw.replace('{gitbranch}', gitbranch); + if (gitreponame) raw = raw.replace('{gitreponame}', gitreponame); } return raw; @@ -327,6 +332,22 @@ export default class Activity implements Disposable { } fileDetail.size = `${originalSize > 1000 ? size.toFixed(2) : size}${sizes[currentDivision]}`; } + + if (str.includes('{gitbranch}')) { + if (this.client.git.repositories.length) { + fileDetail.gitbranch = this.client.git.repositories.find((repo) => repo.ui.selected)!.state.HEAD!.name; + } else { + fileDetail.gitbranch = 'Unknown'; + } + } + + if (str.includes('{gitreponame}')) { + if (this.client.git.repositories.length) { + fileDetail.gitreponame = this.client.git.repositories.find((repo) => repo.ui.selected)!.state.remotes[0].fetchUrl!.split('/')[1].replace('.git', ''); + } else { + fileDetail.gitreponame = 'Unknown'; + } + } } return fileDetail;