refactor: big rewrite

This commit is contained in:
iCrawl 2021-02-10 04:24:42 +01:00
parent 3b3f41af23
commit aa5e0c97da
No known key found for this signature in database
GPG key ID: 1AB888B16355FBB2
21 changed files with 4232 additions and 1403 deletions

View file

@ -1,23 +0,0 @@
name: Lint
on: [push, pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install pnpm
run: curl -L https://unpkg.com/@pnpm/self-installer | node
- name: Install dependencies
run: pnpm i
- name: Run Lint
uses: icrawl/action-eslint@v1

23
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Testing
on: [push, pull_request]
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TSC
run: npm run build

View file

@ -1,23 +0,0 @@
name: TSC
on: [push, pull_request]
jobs:
tsc:
name: TSC
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install pnpm
run: curl -L https://unpkg.com/@pnpm/self-installer | node
- name: Install dependencies
run: pnpm i
- name: Run TSC
uses: icrawl/action-tsc@v1

8
.prettierrc.json Normal file
View file

@ -0,0 +1,8 @@
{
"printWidth": 120,
"useTabs": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2017-2020 iCrawl Copyright (c) 2017-2021 iCrawl
Copyright (c) 2017-2019 Khinenw Copyright (c) 2017-2019 Khinenw
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View file

@ -1,4 +1,5 @@
# Discord Presence # Discord Presence
> Update your discord status with the newly added rich presence. > Update your discord status with the newly added rich presence.
<div align="center"> <div align="center">
@ -14,19 +15,20 @@
## Features ## Features
* Shows what you are editing in VSCode with no bullsh*t involved - Shows what you are editing in VSCode with no bullsh\*t involved
* Support for over 130 of the most popular languages - Support for over 130 of the most popular languages
* Enable/Disable Rich Presence for individual workspaces (enabled by default) - Enable/Disable Rich Presence for individual workspaces (enabled by default)
* Custom string support - Custom string support
* Respects Discords 15sec limit when it comes to updating your status - Respects Discords 15sec limit when it comes to updating your status
* Stable or Insiders build detection - Stable or Insiders build detection
* Debug mode detection - Debug mode detection
* Easily manually reconnect to Discord - Easily manually reconnect to Discord
* VSCode Live Share support - VSCode Live Share support
## Troubleshooting ## Troubleshooting
### Can't connect to Discord? Check those: ### Can't connect to Discord? Check those:
https://github.com/iCrawl/discord-vscode/issues/77#issuecomment-435622205 https://github.com/iCrawl/discord-vscode/issues/77#issuecomment-435622205
https://github.com/iCrawl/discord-vscode/issues/85#issuecomment-417895483 https://github.com/iCrawl/discord-vscode/issues/85#issuecomment-417895483

3291
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "discord-vscode", "name": "discord-vscode",
"displayName": "Discord Presence", "displayName": "Discord Presence",
"version": "4.1.0", "version": "5.0.0",
"description": "Update your discord status with the newly added rich presence.", "description": "Update your discord status with the newly added rich presence.",
"private": true, "private": true,
"author": { "author": {
@ -20,7 +20,6 @@
"scripts": { "scripts": {
"prebuild": "npm run lint", "prebuild": "npm run lint",
"build": "webpack --mode production", "build": "webpack --mode production",
"tsc": "tsc",
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix" "lint:fix": "eslint src --ext .ts --fix"
}, },
@ -34,42 +33,22 @@
"commands": [ "commands": [
{ {
"command": "discord.enable", "command": "discord.enable",
"title": "Enable Discord Presence in the Current Workspace", "title": "Enable Discord Presence in the current workspace",
"category": "Discord Presence" "category": "Discord Presence"
}, },
{ {
"command": "discord.disable", "command": "discord.disable",
"title": "Disable Discord Presence in the Current Workspace", "title": "Disable Discord Presence in the current workspace",
"category": "Discord Presence" "category": "Discord Presence"
}, },
{ {
"command": "discord.reconnect", "command": "discord.reconnect",
"title": "Reconnect Discord Presence to Discord RPC", "title": "Reconnect Discord Presence to Discord",
"category": "Discord Presence" "category": "Discord Presence"
}, },
{ {
"command": "discord.disconnect", "command": "discord.disconnect",
"title": "Disconnect Discord Presence from Discord RPC", "title": "Disconnect Discord Presence from Discord",
"category": "Discord Presence"
},
{
"command": "discord.allowSpectate",
"title": "Allow Spectating",
"category": "Discord Presence"
},
{
"command": "discord.disableSpectate",
"title": "Disable Spectating",
"category": "Discord Presence"
},
{
"command": "discord.allowJoinRequests",
"title": "Allow Join Requests",
"category": "Discord Presence"
},
{
"command": "discord.disableJoinRequests",
"title": "Disable Join Requests",
"category": "Discord Presence" "category": "Discord Presence"
} }
], ],
@ -83,67 +62,67 @@
"default": true, "default": true,
"description": "Controls if the Discord Presence should show across all workspaces" "description": "Controls if the Discord Presence should show across all workspaces"
}, },
"discord.detailsEditing": { "discord.details_idling": {
"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- '{workspaceFolder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspaceAndFolder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\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- '{workspaceFolder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspaceAndFolder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\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", "type": "string",
"default": "Idling", "default": "Idling",
"description": "Custom string for the details section of the rich presence when idling\n\t- '{null}' will be replaced with an empty space." "description": "Custom string for the details section of the rich presence when idling\n\t- '{empty}' will be replaced with an empty space."
}, },
"discord.lowerDetailsEditing": { "discord.details_editing": {
"type": "string",
"default": "Editing {file_name}",
"description": "Custom string for the details section of the rich presence\n\t- '{empty}' will be replaced with an empty space.\n\t- '{file_name}' will be replaced with the current file name.\n\t- '{dir_name}' will get replaced with the folder name that has the current file.\n\t- '{full_dir_name}' 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- '{workspace_folder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspace_and_folder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\n\t- '{current_column}' will get replaced with the current column of the current line.\n\t- '{current_line}' will get replaced with the current line number.\n\t- '{total_lines}' will get replaced with the total line number.\n\t- '{file_size}' will get replaced with the current file's size.\n\t- '{git_repo_name}' will be replaced with the active Git repository name (from the git URL)\n\t- '{git_branch}' will be replaced with the current active branch name."
},
"discord.details_debugging": {
"type": "string",
"default": "Debugging {file_name}",
"description": "Custom string for the details section of the rich presence when debugging\n\t- '{empty}' will be replaced with an empty space.\n\t- '{file_name}' will be replaced with the current file name.\n\t- '{dir_name}' will get replaced with the folder name that has the current file.\n\t- '{full_dir_name}' 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- '{workspace_folder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspace_and_folder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\n\t- '{current_column}' will get replaced with the current column of the current line.\n\t- '{current_line}' will get replaced with the current line number.\n\t- '{total_lines}' will get replaced with the total line number.\n\t- '{file_size}' will get replaced with the current file's size.\n\t- '{git_repo_name}' will be replaced with the active Git repository name (from the git URL)\n\t- '{git_branch}' will be replaced with the current active branch name."
},
"discord.lower_details_idling": {
"type": "string",
"default": "Idling",
"description": "Custom string for the state section of the rich presence when idling\n\t- '{empty}' will be replaced with an empty space."
},
"discord.lower_details_editing": {
"type": "string", "type": "string",
"default": "Workspace: {workspace}", "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- '{workspaceFolder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspaceAndFolder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\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." "description": "Custom string for the state section of the rich presence\n\t- '{empty}' will be replaced with an empty space.\n\t- '{file_name}' will be replaced with the current file name.\n\t- '{dir_name}' will get replaced with the folder name that has the current file.\n\t- '{full_dir_name}' 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- '{workspace_folder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspace_and_folder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\n\t- '{current_column}' will get replaced with the current column of the current line.\n\t- '{current_line}' will get replaced with the current line number.\n\t- '{total_lines}' will get replaced with the total line number.\n\t- '{file_size}' will get replaced with the current file's size.\n\t- '{git_repo_name}' will be replaced with the active Git repository name (from the git URL)\n\t- '{git_branch}' will be replaced with the current active branch name."
}, },
"discord.lowerDetailsDebugging": { "discord.lower_details_debugging": {
"type": "string", "type": "string",
"default": "Debugging: {workspace}", "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- '{workspaceFolder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspaceAndFolder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\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." "description": "Custom string for the state section of the rich presence when debugging\n\t- '{empty}' will be replaced with an empty space.\n\t- '{file_name}' will be replaced with the current file name.\n\t- '{dir_name}' will get replaced with the folder name that has the current file.\n\t- '{full_dir_name}' 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- '{workspace_folder}' will be replaced with the currently accessed workspace folder, if any.\n\t- '{workspace_and_folder} will be replaced with the currently accessed workspace and workspace folder like this: 'Workspace - WorkspaceFolder'\n\t- '{current_column}' will get replaced with the current column of the current line.\n\t- '{current_line}' will get replaced with the current line number.\n\t- '{total_lines}' will get replaced with the total line number.\n\t- '{file_size}' will get replaced with the current file's size.\n\t- '{git_repo_name}' will be replaced with the active Git repository name (from the git URL)\n\t- '{git_branch}' will be replaced with the current active branch name."
}, },
"discord.lowerDetailsIdle": { "discord.lower_details_no_workspace_found": {
"type": "string",
"default": "Idling",
"description": "Custom string for the state section of the rich presence when idling\n\t- '{null}' will be replaced with an empty space."
},
"discord.lowerDetailsNotFound": {
"type": "string", "type": "string",
"default": "No workspace.", "default": "No workspace.",
"description": "Custom string for the state section of the rich presence when no workspace is found.\nIf set to '{null}', this will be an empty space.\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 no workspace is found.\nIf set to '{empty}', this will be an empty space.\n\t- '{current_line}' will get replaced with the current line number.\n\t- '{total_lines}' will get replaced with the total line number.\n\t- '{file_size}' will get replaced with the current file's size."
}, },
"discord.largeImage": { "discord.large_image_idling": {
"type": "string",
"default": "Editing a {LANG} file",
"description": "Custom string for the largeImageText section of the rich presence.\n\t- '{lang}' will be replaced with the lowercased language ID\n\t- '{LANG}' will be replaced with the uppercased language ID"
},
"discord.largeImageIdle": {
"type": "string", "type": "string",
"default": "Idling", "default": "Idling",
"description": "Custom string for the largeImageText section of the rich presence when idling" "description": "Custom string for the largeImageText section of the rich presence when idling"
}, },
"discord.smallImage": { "discord.large_image": {
"type": "string", "type": "string",
"default": "{appname}", "default": "Editing a {LANG} file",
"description": "Custom string for the smallImageText section of the rich presence\n\t- '{appname}' will get replaced with the current Visual Studio Code version." "description": "Custom string for the largeImageText section of the rich presence.\n\t- '{lang}' will be replaced with the lowercased language ID\n\t- '{LANG}' will be replaced with the uppercased language ID"
}, },
"discord.silent": { "discord.small_image": {
"type": "string",
"default": "{app_name}",
"description": "Custom string for the smallImageText section of the rich presence\n\t- '{app_name}' will get replaced with the current Visual Studio Code version."
},
"discord.suppress_notifications": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"description": "Decides if error messages are shown to the user" "description": "Decides if error messages are shown to the user"
}, },
"discord.workspaceElapsedTime": { "discord.workspace_elapsed_time": {
"type": "boolean", "type": "boolean",
"default": false, "default": true,
"description": "Decides whether to display elapsed time for a workspace or a single file" "description": "Decides whether to display elapsed time for a workspace or a single file"
}, },
"discord.workspaceExcludePatterns": { "discord.workspace_exclude_patterns": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
@ -181,10 +160,10 @@
}, },
"dependencies": { "dependencies": {
"bufferutil": "^4.0.3", "bufferutil": "^4.0.3",
"dayjs": "^1.10.4",
"discord-rpc": "github:discordjs/RPC", "discord-rpc": "github:discordjs/RPC",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"utf-8-validate": "^5.0.4", "utf-8-validate": "^5.0.4"
"vsls": "^1.0.3015"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^14.14.25", "@types/node": "^14.14.25",
@ -193,7 +172,7 @@
"@typescript-eslint/parser": "^4.15.0", "@typescript-eslint/parser": "^4.15.0",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"eslint": "^7.19.0", "eslint": "^7.19.0",
"eslint-config-marine": "^7.2.0", "eslint-config-marine": "^8.1.0",
"eslint-config-prettier": "^7.2.0", "eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1", "prettier": "^2.2.1",
@ -204,6 +183,6 @@
"webpack-cli": "^4.5.0" "webpack-cli": "^4.5.0"
}, },
"engines": { "engines": {
"vscode": "^1.51.0" "vscode": "^1.53.0"
} }
} }

193
src/activity.ts Normal file
View file

@ -0,0 +1,193 @@
import { basename, parse, sep } from 'path';
import { debug, env, extensions, Selection, TextDocument, window, workspace } from 'vscode';
import {
CONFIG_KEYS,
DEBUG_IMAGE_KEY,
EMPTY,
FILE_SIZES,
IDLE_IMAGE_KEY,
REPLACE_KEYS,
VSCODE_IMAGE_KEY,
VSCODE_INSIDERS_IMAGE_KEY,
} from './constants';
import { GitExtension } from './git';
import { log, LogLevel } from './logger';
import { getConfig, resolveFileIcon, toLower, toTitle, toUpper } from './util';
interface ActivityPayload {
details: string;
state?: string;
startTimestamp?: number | null;
largeImageKey?: string;
largeImageText?: string;
smallImageKey?: string;
smallImageText?: string;
partyId?: string;
partySize?: number;
partyMax?: number;
matchSecret?: string;
joinSecret?: string;
spectateSecret?: string;
instance?: boolean;
}
export async function activity() {
const config = getConfig();
const appName = env.appName;
const defaultSmallImageKey = debug.activeDebugSession
? DEBUG_IMAGE_KEY
: appName.includes('Insiders')
? VSCODE_INSIDERS_IMAGE_KEY
: VSCODE_IMAGE_KEY;
let state: ActivityPayload = {
details: await details(CONFIG_KEYS.DetailsIdling, CONFIG_KEYS.DetailsEditing, CONFIG_KEYS.DetailsDebugging),
state: await details(
CONFIG_KEYS.LowerDetailsIdling,
CONFIG_KEYS.LowerDetailsEditing,
CONFIG_KEYS.LowerDetailsDebugging,
),
startTimestamp: null,
largeImageKey: IDLE_IMAGE_KEY,
largeImageText: config[CONFIG_KEYS.LargeImageIdling],
smallImageKey: defaultSmallImageKey,
smallImageText: config[CONFIG_KEYS.SmallImage].replace(REPLACE_KEYS.AppName, appName),
};
if (window.activeTextEditor) {
const largeImageKey = resolveFileIcon(window.activeTextEditor.document);
const largeImageText = config[CONFIG_KEYS.LargeImage]
.replace(REPLACE_KEYS.LanguageLowerCase, toLower(largeImageKey))
.replace(REPLACE_KEYS.LanguageTitleCase, toTitle(largeImageKey))
.replace(REPLACE_KEYS.LanguageUpperCase, toUpper(largeImageKey))
.padEnd(2, EMPTY);
state = {
...state,
details: await details(CONFIG_KEYS.DetailsIdling, CONFIG_KEYS.DetailsEditing, CONFIG_KEYS.DetailsDebugging),
state: await details(
CONFIG_KEYS.LowerDetailsIdling,
CONFIG_KEYS.LowerDetailsEditing,
CONFIG_KEYS.LowerDetailsDebugging,
),
largeImageKey,
largeImageText,
};
}
log(LogLevel.Debug, JSON.stringify(state, null, 2));
return state;
}
async function details(idling: CONFIG_KEYS, editing: CONFIG_KEYS, debugging: CONFIG_KEYS) {
const config = getConfig();
let raw = (config[idling] as string).replace(REPLACE_KEYS.Empty, EMPTY);
if (window.activeTextEditor) {
const fileName = basename(window.activeTextEditor.document.fileName);
const { dir } = parse(window.activeTextEditor.document.fileName);
const split = dir.split(sep);
const dirName = split[split.length - 1];
const noWorkspaceFound = config[CONFIG_KEYS.LowerDetailsNoWorkspaceFound].replace(REPLACE_KEYS.Empty, EMPTY);
const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri);
const workspaceFolderName = workspaceFolder?.name ?? noWorkspaceFound;
const workspaceName = workspace.name ?? workspaceFolderName;
const workspaceAndFolder = `${workspaceName}${workspaceFolderName === EMPTY ? '' : ` - ${workspaceFolderName}`}`;
const fileIcon = resolveFileIcon(window.activeTextEditor.document);
if (debug.activeDebugSession) {
raw = config[debugging] as string;
} else {
raw = config[editing] as string;
}
if (workspaceFolder) {
const { name } = workspaceFolder;
const relativePath = workspace.asRelativePath(window.activeTextEditor.document.fileName).split(sep);
relativePath.splice(-1, 1);
raw = raw.replace(REPLACE_KEYS.FullDirName, `${name}${sep}${relativePath.join(sep)}`);
}
raw = await fileDetails(raw, window.activeTextEditor.document, window.activeTextEditor.selection);
raw = raw
.replace(REPLACE_KEYS.FileName, fileName)
.replace(REPLACE_KEYS.DirName, dirName)
.replace(REPLACE_KEYS.Workspace, workspaceName)
.replace(REPLACE_KEYS.WorkspaceFolder, workspaceFolderName)
.replace(REPLACE_KEYS.WorkspaceAndFolder, workspaceAndFolder)
.replace(REPLACE_KEYS.LanguageLowerCase, toLower(fileIcon))
.replace(REPLACE_KEYS.LanguageTitleCase, toTitle(fileIcon))
.replace(REPLACE_KEYS.LanguageUpperCase, toUpper(fileIcon));
}
return raw;
}
async function fileDetails(_raw: string, document: TextDocument, selection: Selection) {
let raw = _raw.slice();
const gitExtension = extensions.getExtension<GitExtension>('vscode.git');
const git = gitExtension?.exports.getAPI(1);
if (raw.includes(REPLACE_KEYS.TotalLines)) {
raw = raw.replace(REPLACE_KEYS.TotalLines, document.toLocaleString());
}
if (raw.includes(REPLACE_KEYS.CurrentLine)) {
raw = raw.replace(REPLACE_KEYS.CurrentLine, (selection.active.line + 1).toLocaleString());
}
if (raw.includes(REPLACE_KEYS.CurrentColumn)) {
raw = raw.replace(REPLACE_KEYS.CurrentColumn, (selection.active.character + 1).toLocaleString());
}
if (raw.includes(REPLACE_KEYS.FileSize)) {
let currentDivision = 0;
let { size } = await workspace.fs.stat(document.uri);
const originalSize = size;
if (originalSize > 1000) {
size /= 1000;
currentDivision++;
while (size > 1000) {
currentDivision++;
size /= 1000;
}
}
raw = raw.replace(
REPLACE_KEYS.FileSize,
`${originalSize > 1000 ? size.toFixed(2) : size}${FILE_SIZES[currentDivision]}`,
);
}
if (raw.includes(REPLACE_KEYS.GitBranch)) {
if (git?.repositories.length) {
raw = raw.replace(
REPLACE_KEYS.GitBranch,
git.repositories.find((repo) => repo.ui.selected)?.state.HEAD?.name ?? EMPTY,
);
} else {
raw = raw.replace(REPLACE_KEYS.GitBranch, 'Unknown');
}
}
if (raw.includes(REPLACE_KEYS.GitRepoName)) {
if (git?.repositories.length) {
raw = raw.replace(
REPLACE_KEYS.GitRepoName,
git.repositories
.find((repo) => repo.ui.selected)
?.state.remotes[0].fetchUrl?.split('/')[1]
.replace('.git', '') ?? EMPTY,
);
} else {
raw = raw.replace(REPLACE_KEYS.GitRepoName, 'Unknown');
}
}
return raw;
}

View file

@ -1,172 +0,0 @@
const { Client } = require('discord-rpc'); // eslint-disable-line
import { Disposable, StatusBarItem, Uri, window, workspace, env } from 'vscode';
import * as vsls from 'vsls';
import Activity from '../structures/Activity';
import Logger from '../structures/Logger';
import { API } from '../git';
let activityTimer: NodeJS.Timer | undefined;
export default class RPCClient implements Disposable {
public config = workspace.getConfiguration('discord');
public git?: API;
private rpc: any;
private readonly activity = new Activity(this);
public constructor(private readonly clientId: string, public statusBarIcon: StatusBarItem) {}
public get client() {
return this.rpc;
}
public async setActivity(workspaceElapsedTime = false) {
if (!this.rpc) return;
const activity = await this.activity.generate(workspaceElapsedTime);
if (!activity) return;
Logger.log('Sending activity to Discord.');
this.rpc.setActivity(activity);
}
public allowSpectate() {
if (!this.rpc) return;
Logger.log('Allowed spectating.');
Logger.log('Sending spectate activity to Discord.');
void this.activity.allowSpectate();
}
public disableSpectate() {
if (!this.rpc) return;
Logger.log('Disabled spectating.');
void this.activity.disableSpectate();
}
public allowJoinRequests() {
if (!this.rpc) return;
Logger.log('Allowed join requests.');
Logger.log('Sending join activity to Discord.');
void this.activity.allowJoinRequests();
}
public disableJoinRequests() {
if (!this.rpc) return;
Logger.log('Disabled join requests.');
void this.activity.disableJoinRequests();
}
public async login() {
if (this.rpc) {
this.dispose();
}
this.rpc = new Client({ transport: 'ipc' });
Logger.log('Logging into RPC...');
this.rpc.transport.once('close', () => {
if (!this.config.get<boolean>('enabled')) return;
void this.dispose();
this.statusBarIcon.text = '$(plug) Reconnect to Discord';
this.statusBarIcon.command = 'discord.reconnect';
this.statusBarIcon.tooltip = '';
});
this.rpc.once('ready', async () => {
Logger.log('Successfully connected to Discord.');
this.statusBarIcon.text = '$(globe) Connected to Discord';
this.statusBarIcon.tooltip = 'Connected to Discord';
setTimeout(() => (this.statusBarIcon.text = '$(globe)'), 5000);
if (activityTimer) clearInterval(activityTimer);
void this.setActivity(this.config.get<boolean>('workspaceElapsedTime'));
activityTimer = setInterval(() => {
this.config = workspace.getConfiguration('discord');
void this.setActivity(this.config.get<boolean>('workspaceElapsedTime'));
}, 1000);
this.rpc.subscribe('ACTIVITY_SPECTATE', async ({ secret }: { secret: string }) => {
const liveshare = await vsls.getApi();
if (!liveshare) return;
try {
const s = Buffer.from(secret, 'base64').toString();
// You might be asking yourself: "but why?"
// VS Liveshare has this annoying bug where you convert a URL string to a URI object to autofill
// But the autofill will be empty, so to circumvent this I need to add copying the link to the clipboard
// And immediately pasting it after the window pops up empty
await env.clipboard.writeText(s);
const uriString = await env.clipboard.readText();
const uri = Uri.parse(uriString);
await liveshare.join(uri);
} catch (error) {
Logger.log(error);
}
});
// You might be asking yourself again: "but why?"
// Same here, this is a real nasty race condition that happens inside the discord-rpc module currently
// To circumvent this we need to timeout sending the subscribe events to the discord client
setTimeout(() => {
this.rpc.subscribe(
'ACTIVITY_JOIN_REQUEST',
async ({ user }: { user: { username: string; discriminator: string } }) => {
const val = await window.showInformationMessage(
`${user.username}#${user.discriminator} wants to join your session`,
{ title: 'Accept' },
{ title: 'Decline' },
);
if (val && val.title === 'Accept') await this.rpc.sendJoinInvite(user);
else await this.rpc.closeJoinRequest(user);
},
);
}, 1000);
setTimeout(() => {
this.rpc.subscribe('ACTIVITY_JOIN', async ({ secret }: { secret: string }) => {
const liveshare = await vsls.getApi();
if (!liveshare) return;
try {
const s = Buffer.from(secret, 'base64').toString();
// You might be asking yourself again again: "but why?"
// See first comment above
await env.clipboard.writeText(s);
const uriString = await env.clipboard.readText();
const uri = Uri.parse(uriString);
await liveshare.join(uri);
} catch (error) {
Logger.log(error);
}
});
}, 2000);
const liveshare = await vsls.getApi();
if (!liveshare) return;
liveshare.onDidChangeSession(({ session }) => {
if (session.id) return this.activity.changePartyId(session.id);
return this.activity.changePartyId();
});
liveshare.onDidChangePeers(({ added, removed }) => {
if (added.length) return this.activity.increasePartySize(added.length);
else if (removed.length) return this.activity.decreasePartySize(removed.length);
});
});
try {
await this.rpc.login({ clientId: this.clientId });
} catch (error) {
throw error;
}
}
public dispose() {
this.activity.dispose();
if (this.rpc) this.rpc.destroy();
this.rpc = null;
this.statusBarIcon.tooltip = '';
if (activityTimer) clearInterval(activityTimer);
}
}

View file

@ -1,8 +1,51 @@
export const LIVE_SHARE_BASE_URL = 'insiders.liveshare.vsengsaas.visualstudio.com'; import LANG from './data/languages.json';
export const VSLS_EXTENSION_ID = 'ms-vsliveshare.vsliveshare';
export const LIVE_SHARE_COMMANDS = { export const CLIENT_ID = '383226320970055681' as const;
Start: 'liveshare.start',
End: 'liveshare.end', export const KNOWN_EXTENSIONS: { [key: string]: { image: string } } = LANG.KNOWN_EXTENSIONS;
Join: 'liveshare.join', export const KNOWN_LANGUAGES: { language: string; image: string }[] = LANG.KNOWN_LANGUAGES;
};
export const EMPTY = '\u200b\u200b';
export const FILE_SIZES = [' bytes', 'kb', 'mb', 'gb', 'tb'];
export const IDLE_IMAGE_KEY = 'vscode-big';
export const DEBUG_IMAGE_KEY = 'debug';
export const VSCODE_IMAGE_KEY = 'vscode';
export const VSCODE_INSIDERS_IMAGE_KEY = 'vscode-insiders';
export const enum REPLACE_KEYS {
Empty = '{empty}',
FileName = '{file_name}',
DirName = '{dir_name}',
FullDirName = '{full_dir_name}',
Workspace = '{workspace}',
WorkspaceFolder = '{workspace_folder}',
WorkspaceAndFolder = '{workspace_and_folder}',
LanguageLowerCase = '{lang}',
LanguageTitleCase = '{Lang}',
LanguageUpperCase = '{LANG}',
TotalLines = '{total_lines}',
CurrentLine = '{current_line}',
CurrentColumn = '{current_column}',
FileSize = '{file_size}',
AppName = '{app_name}',
GitRepoName = '{git_repo_name}',
GitBranch = '{git_branch}',
}
export const enum CONFIG_KEYS {
Enabled = 'enabled',
DetailsIdling = 'details_idling',
DetailsEditing = 'details_editing',
DetailsDebugging = 'details_debugging',
LowerDetailsIdling = 'lower_details_idling',
LowerDetailsEditing = 'lower_details_editing',
LowerDetailsDebugging = 'lower_details_debugging',
LowerDetailsNoWorkspaceFound = 'lower_details_no_workspace_found',
LargeImageIdling = 'large_image_idling',
LargeImage = 'large_image',
SmallImage = 'small_image',
SuppressNotifications = 'suppress_notifications',
WorkspaceElapsedTime = 'workspace_elapsed_time',
WorkspaceExcludePatterns = 'workspace_exclude_patterns',
}

View file

@ -1,145 +1,68 @@
{ {
"knownLanguages": [ "KNOWN_LANGUAGES": [
"ahk", { "language": "abap", "image": "text" },
"android", { "language": "bat", "image": "bat" },
"angular", { "language": "bibtex", "image": "text" },
"applescript", { "language": "clojure", "image": "clojure" },
"appveyor", { "language": "coffeescript", "image": "coffeescript" },
"arduino", { "language": "c", "image": "c" },
"as", { "language": "cpp", "image": "cpp" },
"asp", { "language": "csharp", "image": "csharp" },
"assembly", { "language": "css", "image": "css" },
"autoit", { "language": "diff", "image": "manifest" },
"babel", { "language": "dockerfile", "image": "docker" },
"bat", { "language": "fsharp", "image": "fsharp" },
"bower", { "language": "git-commit", "image": "manifest" },
"brainfuck", { "language": "git-rebase", "image": "manifest" },
"c", { "language": "go", "image": "go" },
"cargo", { "language": "groovy", "image": "groovy" },
"cfml", { "language": "handlebars", "image": "handlebars" },
"circleci", { "language": "haml", "image": "text" },
"clojure", { "language": "html", "image": "html" },
"cmake", { "language": "ini", "image": "manifest" },
"codeclimate", { "language": "java", "image": "java" },
"coffee", { "language": "javascript", "image": "javascript" },
"cpp", { "language": "javascriptreact", "image": "jsx" },
"crystal", { "language": "jsx", "image": "jsx" },
"csharp", { "language": "json", "image": "json" },
"cshtml", { "language": "jsonc", "image": "json" },
"css", { "language": "latex", "image": "text" },
"cuda", { "language": "less", "image": "less" },
"d", { "language": "lua", "image": "lua" },
"dart", { "language": "makefile", "image": "makefile" },
"delphi", { "language": "markdown", "image": "markdown" },
"denizen", { "language": "objective-c", "image": "objective-c" },
"dm", { "language": "objective-cpp", "image": "objective-c" },
"docker", { "language": "perl", "image": "perl" },
"editorconfig", { "language": "perl6", "image": "perl" },
"ejs", { "language": "php", "image": "php" },
"elixir", { "language": "plaintext", "image": "text" },
"elm", { "language": "powershell", "image": "powershell" },
"env", { "language": "jade", "image": "pug" },
"erlang", { "language": "pug", "image": "pug" },
"eslint", { "language": "python", "image": "python" },
"flowconfig", { "language": "r", "image": "r" },
"fsharp", { "language": "razor", "image": "cshtml" },
"gemfile", { "language": "ruby", "image": "ruby" },
"git", { "language": "rust", "image": "rust" },
"go", { "language": "scss", "image": "scss" },
"godot", { "language": "sass", "image": "scss" },
"gradle", { "language": "shaderlab", "image": "manifest" },
"graphql", { "language": "slim", "image": "text" },
"groovy", { "language": "sql", "image": "sql" },
"gruntfile", { "language": "stylus", "image": "stylus" },
"gulp", { "language": "swift", "image": "swift" },
"handlebars", { "language": "typescript", "image": "typescript" },
"harbour", { "language": "typescriptreact", "image": "tsx" },
"haskell", { "language": "tex", "image": "tex" },
"haxe", { "language": "vb", "image": "vb" },
"heroku", { "language": "vue", "image": "vue" },
"hjson", { "language": "vue-html", "image": "vue" },
"html", { "language": "xml", "image": "xml" },
"http", { "language": "xsl", "image": "xml" },
"ini", { "language": "yaml", "image": "yaml" }
"jar",
"java",
"jest",
"jinja",
"js",
"jsmap",
"json",
"jsx",
"julia",
"jupyter",
"kotlin",
"less",
"lisp",
"livescript",
"log",
"lua",
"makefile",
"manifest",
"markdown",
"markdownx",
"marko",
"nim",
"nix",
"nodemon",
"npm",
"objc",
"ocaml",
"pascal",
"pawn",
"perl",
"php",
"ponylang",
"postcss",
"powershell",
"prettier",
"prisma",
"processing",
"pug",
"purescript",
"python",
"r",
"reasonml",
"ruby",
"rust",
"scala",
"scss",
"shell",
"sqf",
"sql",
"stylus",
"svelte",
"svg",
"swift",
"tex",
"text",
"toml",
"travis",
"ts",
"tsx",
"twig",
"typescript-def",
"v",
"v",
"vala",
"vb",
"vba",
"vbhtml",
"vbproj",
"vcxproj",
"vscodeignore",
"vue",
"wasm",
"webpack",
"xml",
"yaml",
"yarn",
"zig"
], ],
"knownExtensions": { "KNOWN_EXTENSIONS": {
".ahk": { "image": "ahk" }, ".ahk": { "image": "ahk" },
".ahkl": { "image": "ahk" }, ".ahkl": { "image": "ahk" },
"androidmanifest.xml": { "image": "android" }, "androidmanifest.xml": { "image": "android" },
@ -193,12 +116,12 @@
".cmake": { "image": "cmake" }, ".cmake": { "image": "cmake" },
"/^CMakeLists\\.txt$/": { "image": "cmake" }, "/^CMakeLists\\.txt$/": { "image": "cmake" },
"/\\.codeclimate\\.(yml|json)/i": { "image": "codeclimate" }, "/\\.codeclimate\\.(yml|json)/i": { "image": "codeclimate" },
".coffee": { "image": "coffee" }, ".coffee": { "image": "coffeescript" },
".cjsx": { "image": "coffee" }, ".cjsx": { "image": "coffeescript" },
".coffee.ecr": { "image": "coffee" }, ".coffee.ecr": { "image": "coffeescript" },
".coffee.erb": { "image": "coffee" }, ".coffee.erb": { "image": "coffeescript" },
".litcoffee": { "image": "coffee" }, ".litcoffee": { "image": "coffeescript" },
".iced": { "image": "coffee" }, ".iced": { "image": "coffeescript" },
"/\\.c[+px]{2}$|\\.cc$/i": { "image": "cpp" }, "/\\.c[+px]{2}$|\\.cc$/i": { "image": "cpp" },
"/\\.h[+px]{2}$/i": { "image": "cpp" }, "/\\.h[+px]{2}$/i": { "image": "cpp" },
"/\\.[it]pp$/i": { "image": "cpp" }, "/\\.[it]pp$/i": { "image": "cpp" },
@ -285,10 +208,10 @@
".java": { "image": "java" }, ".java": { "image": "java" },
"jest.config.js": { "image": "jest" }, "jest.config.js": { "image": "jest" },
".jinja": { "image": "jinja" }, ".jinja": { "image": "jinja" },
".js": { "image": "js" }, ".js": { "image": "javascript" },
".es6": { "image": "js" }, ".es6": { "image": "javascript" },
".es": { "image": "js" }, ".es": { "image": "javascript" },
".mjs": { "image": "js" }, ".mjs": { "image": "javascript" },
".js.map": { "image": "jsmap" }, ".js.map": { "image": "jsmap" },
".json": { "image": "json" }, ".json": { "image": "json" },
".jsonc": { "image": "json" }, ".jsonc": { "image": "json" },
@ -360,9 +283,9 @@
".nix": { "image": "nix" }, ".nix": { "image": "nix" },
"nodemon.json": { "image": "nodemon" }, "nodemon.json": { "image": "nodemon" },
".npmrc": { "image": "npm" }, ".npmrc": { "image": "npm" },
"/\\.mm?$/i": { "image": "objc" }, "/\\.mm?$/i": { "image": "objective-c" },
".pch": { "image": "objc" }, ".pch": { "image": "objective-c" },
".x": { "image": "objc" }, ".x": { "image": "objective-c" },
".ml": { "image": "ocaml" }, ".ml": { "image": "ocaml" },
".mli": { "image": "ocaml" }, ".mli": { "image": "ocaml" },
".eliom": { "image": "ocaml" }, ".eliom": { "image": "ocaml" },
@ -491,7 +414,7 @@
"/\\.(utxt|utf8)$/i": { "image": "text" }, "/\\.(utxt|utf8)$/i": { "image": "text" },
".toml": { "image": "toml" }, ".toml": { "image": "toml" },
".travis.yml": { "image": "travis" }, ".travis.yml": { "image": "travis" },
".ts": { "image": "ts" }, ".ts": { "image": "typescript" },
".tsx": { "image": "tsx" }, ".tsx": { "image": "tsx" },
".twig": { "image": "twig" }, ".twig": { "image": "twig" },
"/.*\\.d\\.ts/i": { "image": "typescript-def" }, "/.*\\.d\\.ts/i": { "image": "typescript-def" },
@ -510,7 +433,9 @@
".wat": { "image": "wasm" }, ".wat": { "image": "wasm" },
".wast": { "image": "wasm" }, ".wast": { "image": "wasm" },
".wasm": { "image": "wasm" }, ".wasm": { "image": "wasm" },
"/webpack(\\.dev|\\.development|\\.prod|\\.production)?\\.config(\\.babel)?\\.(js|jsx|coffee|ts|json|json5|yaml|yml)/i": { "image": "webpack" }, "/webpack(\\.dev|\\.development|\\.prod|\\.production)?\\.config(\\.babel)?\\.(js|jsx|coffee|ts|json|json5|yaml|yml)/i": {
"image": "webpack"
},
".xml": { "image": "xml" }, ".xml": { "image": "xml" },
"/\\.ya?ml$/i": { "image": "yaml" }, "/\\.ya?ml$/i": { "image": "yaml" },
"/^yarn(\\.lock)?$/i": { "image": "yarn" }, "/^yarn(\\.lock)?$/i": { "image": "yarn" },

View file

@ -1,26 +1,71 @@
import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, window, workspace, extensions } from 'vscode'; const { Client } = require('discord-rpc'); // eslint-disable-line
import RPCClient from './client/RPCClient'; import {
import Logger from './structures/Logger'; commands,
import { GitExtension } from './git'; ExtensionContext,
const { register } = require('discord-rpc'); // eslint-disable-line StatusBarAlignment,
StatusBarItem,
window,
workspace,
extensions,
debug,
} from 'vscode';
import { activity } from './activity';
let loginTimeout: NodeJS.Timer | undefined; import { CLIENT_ID, CONFIG_KEYS } from './constants';
import { GitExtension } from './git';
import { log, LogLevel } from './logger';
import { getConfig } from './util';
const statusBarIcon: StatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); const statusBarIcon: StatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
statusBarIcon.text = '$(pulse) Connecting to Discord...'; statusBarIcon.text = '$(pulse) Connecting to Discord...';
const clientId = '383226320970055681'; const rpc = new Client({ transport: 'ipc' });
const config = workspace.getConfiguration('discord'); const config = getConfig();
register(clientId);
const rpc = new RPCClient(clientId, statusBarIcon);
export async function activate(context: ExtensionContext) { async function sendActivity() {
Logger.log('Discord Presence activated!'); rpc.setActivity(await activity());
}
async function login(context: ExtensionContext) {
rpc.once('ready', () => {
log(LogLevel.Info, 'Successfully connected to Discord');
statusBarIcon.text = '$(globe) Connected to Discord';
statusBarIcon.tooltip = 'Connected to Discord';
void sendActivity();
const onChangeActiveTextEditor = window.onDidChangeActiveTextEditor(() => sendActivity());
const onChangeTextDocument = workspace.onDidChangeTextDocument(() => sendActivity());
const onStartDebugSession = debug.onDidStartDebugSession(() => sendActivity());
const onTerminateDebugSession = debug.onDidTerminateDebugSession(() => sendActivity());
context.subscriptions.push(
onChangeActiveTextEditor,
onChangeTextDocument,
onStartDebugSession,
onTerminateDebugSession,
);
});
try {
await rpc.login({ clientId: CLIENT_ID });
} catch (error) {
log(LogLevel.Error, `Encountered following error while trying to login:\n${error as string}`);
rpc.dispose();
if (!config[CONFIG_KEYS.SuppressNotifications]) {
if (error?.message?.includes('ENOENT')) void window.showErrorMessage('No Discord client detected');
else void window.showErrorMessage(`Couldn't connect to Discord via RPC: ${error as string}`);
}
rpc.statusBarIcon.text = '$(pulse) Reconnect to Discord';
rpc.statusBarIcon.command = 'discord.reconnect';
}
}
export function activate(context: ExtensionContext) {
log(LogLevel.Info, 'Discord Presence activated');
let isWorkspaceExcluded = false; let isWorkspaceExcluded = false;
const excludePatterns = config.get<string[]>('workspaceExcludePatterns'); for (const pattern of config[CONFIG_KEYS.WorkspaceExcludePatterns]) {
if (excludePatterns?.length) {
for (const pattern of excludePatterns) {
const regex = new RegExp(pattern); const regex = new RegExp(pattern);
const folders = workspace.workspaceFolders; const folders = workspace.workspaceFolders;
if (!folders) break; if (!folders) break;
@ -29,84 +74,45 @@ export async function activate(context: ExtensionContext) {
break; break;
} }
} }
}
const enabler = commands.registerCommand('discord.enable', () => { const enabler = commands.registerCommand('discord.enable', () => {
rpc.dispose(); rpc.destroy();
void config.update('enabled', true); void config.update('enabled', true);
rpc.config = workspace.getConfiguration('discord'); statusBarIcon.text = '$(pulse) Connecting to Discord...';
rpc.statusBarIcon.text = '$(pulse) Connecting to Discord...'; statusBarIcon.show();
rpc.statusBarIcon.show(); void login(context);
void rpc.login(); void window.showInformationMessage('Enabled Discord Presence for this workspace');
void window.showInformationMessage('Enabled Discord Rich Presence for this workspace.');
}); });
const disabler = commands.registerCommand('discord.disable', () => { const disabler = commands.registerCommand('discord.disable', () => {
void config.update('enabled', false); void config.update('enabled', false);
rpc.config = workspace.getConfiguration('discord'); rpc.destroy();
rpc.dispose();
rpc.statusBarIcon.hide(); rpc.statusBarIcon.hide();
void window.showInformationMessage('Disabled Discord Rich Presence for this workspace.'); void window.showInformationMessage('Disabled Discord Presence for this workspace');
}); });
const reconnecter = commands.registerCommand('discord.reconnect', () => { const reconnecter = commands.registerCommand('discord.reconnect', () => {
if (loginTimeout) clearTimeout(loginTimeout); deactivate();
rpc.dispose(); void activate(context);
loginTimeout = setTimeout(() => {
void rpc.login();
if (!config.get('silent')) void window.showInformationMessage('Reconnecting to Discord RPC...');
rpc.statusBarIcon.text = '$(pulse) Reconnecting to Discord...';
rpc.statusBarIcon.command = 'discord.reconnect';
}, 1000);
}); });
const disconnect = commands.registerCommand('discord.disconnect', () => { const disconnect = commands.registerCommand('discord.disconnect', () => {
rpc.dispose(); rpc.destroy();
rpc.statusBarIcon.text = '$(pulse) Reconnect to Discord'; rpc.statusBarIcon.text = '$(pulse) Reconnect to Discord';
rpc.statusBarIcon.command = 'discord.reconnect'; rpc.statusBarIcon.command = 'discord.reconnect';
}); });
const allowSpectate = commands.registerCommand('discord.allowSpectate', () => rpc.allowSpectate());
const disableSpectate = commands.registerCommand('discord.disableSpectate', () => rpc.disableSpectate());
const allowJoinRequests = commands.registerCommand('discord.allowJoinRequests', () => rpc.allowJoinRequests());
const disableJoinRequests = commands.registerCommand('discord.disableJoinRequests', () => rpc.disableJoinRequests());
context.subscriptions.push( context.subscriptions.push(enabler, disabler, reconnecter, disconnect);
enabler,
disabler,
reconnecter,
disconnect,
allowSpectate,
disableSpectate,
allowJoinRequests,
disableJoinRequests,
);
if (!isWorkspaceExcluded && config.get<boolean>('enabled')) { if (!isWorkspaceExcluded && config[CONFIG_KEYS.Enabled]) {
statusBarIcon.show(); statusBarIcon.show();
try { void login(context);
await rpc.login();
} catch (error) {
Logger.log(`Encountered following error after trying to login:\n${error as string}`);
rpc.dispose();
if (!config.get('silent')) {
if (error?.message?.includes('ENOENT')) void window.showErrorMessage('No Discord Client detected!');
else void window.showErrorMessage(`Couldn't connect to Discord via RPC: ${error as string}`);
}
rpc.statusBarIcon.text = '$(pulse) Reconnect to Discord';
rpc.statusBarIcon.command = 'discord.reconnect';
}
} }
const gitExtension = extensions.getExtension<GitExtension>('vscode.git'); const gitExtension = extensions.getExtension<GitExtension>('vscode.git');
if (gitExtension) { void gitExtension?.activate();
if (gitExtension.isActive) {
rpc.git = gitExtension.exports.getAPI(1);
} else {
const extension = await gitExtension.activate();
rpc.git = extension.getAPI(1);
}
}
} }
export function deactivate() { export function deactivate() {
rpc.dispose(); rpc.destroy();
} }
process.on('unhandledRejection', (err) => Logger.log(err as string));

91
src/git.d.ts vendored
View file

@ -1,10 +1,10 @@
/* /*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*/ *--------------------------------------------------------------------------------------------*/
// eslint-disable-next-line import { Uri, Event, Disposable, ProviderResult } from 'vscode';
import { Uri, SourceControlInputBox, Event, CancellationToken } from 'vscode'; export { ProviderResult } from 'vscode';
export interface Git { export interface Git {
readonly path: string; readonly path: string;
@ -14,6 +14,11 @@ export interface InputBox {
value: string; value: string;
} }
export const enum ForcePushMode {
Force,
ForceWithLease,
}
export const enum RefType { export const enum RefType {
Head, Head,
RemoteHead, RemoteHead,
@ -42,7 +47,10 @@ export interface Commit {
readonly hash: string; readonly hash: string;
readonly message: string; readonly message: string;
readonly parents: string[]; readonly parents: string[];
readonly authorEmail?: string | undefined; readonly authorDate?: Date;
readonly authorName?: string;
readonly authorEmail?: string;
readonly commitDate?: Date;
} }
export interface Submodule { export interface Submodule {
@ -117,6 +125,24 @@ export interface RepositoryUIState {
export interface LogOptions { export interface LogOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */ /** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number; readonly maxEntries?: number;
readonly path?: string;
}
export interface CommitOptions {
all?: boolean | 'tracked';
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
empty?: boolean;
noVerify?: boolean;
requireUserConfig?: boolean;
}
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
readonly contains?: string;
} }
export interface Repository { export interface Repository {
@ -157,6 +183,7 @@ export interface Repository {
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>; createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
deleteBranch(name: string, force?: boolean): Promise<void>; deleteBranch(name: string, force?: boolean): Promise<void>;
getBranch(name: string): Promise<Branch>; getBranch(name: string): Promise<Branch>;
getBranches(query: BranchQuery): Promise<Ref[]>;
setBranchUpstream(name: string, upstream: string): Promise<void>; setBranchUpstream(name: string, upstream: string): Promise<void>;
getMergeBase(ref1: string, ref2: string): Promise<string>; getMergeBase(ref1: string, ref2: string): Promise<string>;
@ -166,24 +193,75 @@ export interface Repository {
addRemote(name: string, url: string): Promise<void>; addRemote(name: string, url: string): Promise<void>;
removeRemote(name: string): Promise<void>; removeRemote(name: string): Promise<void>;
renameRemote(name: string, newName: string): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>; fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>; pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>; push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
blame(path: string): Promise<string>; blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>; log(options?: LogOptions): Promise<Commit[]>;
commit(message: string, opts?: CommitOptions): Promise<void>;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
readonly icon?: string; // codicon name
readonly supportsQuery?: boolean;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
getBranches?(url: string): ProviderResult<string[]>;
publishRepository?(repository: Repository): Promise<void>;
}
export interface Credentials {
readonly username: string;
readonly password: string;
}
export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export interface PushErrorHandler {
handlePushError(
repository: Repository,
remote: Remote,
refspec: string,
error: Error & { gitErrorCode: GitErrorCodes },
): Promise<boolean>;
} }
export type APIState = 'uninitialized' | 'initialized'; export type APIState = 'uninitialized' | 'initialized';
export interface PublishEvent {
repository: Repository;
branch?: string;
}
export interface API { export interface API {
readonly state: APIState; readonly state: APIState;
readonly onDidChangeState: Event<APIState>; readonly onDidChangeState: Event<APIState>;
readonly onDidPublish: Event<PublishEvent>;
readonly git: Git; readonly git: Git;
readonly repositories: Repository[]; readonly repositories: Repository[];
readonly onDidOpenRepository: Event<Repository>; readonly onDidOpenRepository: Event<Repository>;
readonly onDidCloseRepository: Event<Repository>; readonly onDidCloseRepository: Event<Repository>;
toGitUri(uri: Uri, ref: string): Uri;
getRepository(uri: Uri): Repository | null;
init(root: Uri): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
} }
export interface GitExtension { export interface GitExtension {
@ -220,6 +298,7 @@ export const enum GitErrorCodes {
CantOpenResource = 'CantOpenResource', CantOpenResource = 'CantOpenResource',
GitNotFound = 'GitNotFound', GitNotFound = 'GitNotFound',
CantCreatePipe = 'CantCreatePipe', CantCreatePipe = 'CantCreatePipe',
PermissionDenied = 'PermissionDenied',
CantAccessRemote = 'CantAccessRemote', CantAccessRemote = 'CantAccessRemote',
RepositoryNotFound = 'RepositoryNotFound', RepositoryNotFound = 'RepositoryNotFound',
RepositoryIsLocked = 'RepositoryIsLocked', RepositoryIsLocked = 'RepositoryIsLocked',

33
src/logger.ts Normal file
View file

@ -0,0 +1,33 @@
import { window } from 'vscode';
import dayjs from 'dayjs';
const outputChannel = window.createOutputChannel('Discord Presence');
export const enum LogLevel {
Trace = 'TRACE',
Debug = 'DEBUG',
Info = 'INFO',
Warn = 'WARN',
Error = 'ERROR',
}
function send(level: string, message: string) {
outputChannel.appendLine(`[${dayjs().format('DD/MM/YYYY HH:mm:ss')} - ${level}] ${message}`);
}
export function log(level: LogLevel, message: string | Error) {
if (typeof message === 'string') {
send(level, message);
} else if (message instanceof Error) {
send(level, message.message);
if (message.stack) {
send(level, message.stack);
}
} else if (typeof message === 'object') {
try {
const json = JSON.stringify(message, null, 2);
send(level, json);
} catch {}
}
}

View file

@ -1,376 +0,0 @@
import { basename, parse, sep } from 'path';
import { debug, Disposable, env, window, workspace } from 'vscode';
import * as vsls from 'vsls';
import RPCClient from '../client/RPCClient';
import lang from '../data/languages.json';
const knownExtensions: { [key: string]: { image: string } } = lang.knownExtensions;
const knownLanguages: string[] = lang.knownLanguages;
const empty = '\u200b\u200b';
const sizes = [' bytes', 'kb', 'mb', 'gb', 'tb'];
export interface State {
details?: string;
state?: string;
startTimestamp?: number | null;
largeImageKey?: string;
largeImageText?: string;
smallImageKey?: string;
smallImageText?: string;
partyId?: string;
partySize?: number;
partyMax?: number;
matchSecret?: string;
joinSecret?: string;
spectateSecret?: string;
instance?: boolean;
}
interface FileDetail {
size?: string;
totalLines?: string;
currentLine?: string;
currentColumn?: string;
gitbranch?: string;
gitreponame?: string;
}
export default class Activity implements Disposable {
private _state: State | null = null;
private lastKnownFile = '';
public constructor(private readonly client: RPCClient) {}
public get state() {
return this._state;
}
public async generate(workspaceElapsedTime = false) {
let largeImageKey: any = 'vscode-big';
if (window.activeTextEditor) {
if (window.activeTextEditor.document.languageId === 'Log') return this._state;
if (this._state && window.activeTextEditor.document.fileName === this.lastKnownFile) {
return (this._state = {
...this._state,
details: await this._generateDetails(
'detailsDebugging',
'detailsEditing',
'detailsIdle',
this._state.largeImageKey,
),
smallImageKey: debug.activeDebugSession
? 'debug'
: env.appName.includes('Insiders')
? 'vscode-insiders'
: 'vscode',
state: await this._generateDetails(
'lowerDetailsDebugging',
'lowerDetailsEditing',
'lowerDetailsIdle',
this._state.largeImageKey,
),
});
}
this.lastKnownFile = window.activeTextEditor.document.fileName;
const filename = basename(window.activeTextEditor.document.fileName);
largeImageKey =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
knownExtensions[
Object.keys(knownExtensions).find((key) => {
if (filename.endsWith(key)) return true;
const match = /^\/(.*)\/([mgiy]+)$/.exec(key);
if (!match) return false;
const regex = new RegExp(match[1], match[2]);
return regex.test(filename);
})!
] ??
(knownLanguages.includes(window.activeTextEditor.document.languageId)
? window.activeTextEditor.document.languageId
: null);
}
let previousTimestamp = null;
if (this._state?.startTimestamp) previousTimestamp = this._state.startTimestamp;
this._state = {
...this._state,
details: await this._generateDetails('detailsDebugging', 'detailsEditing', 'detailsIdle', largeImageKey),
startTimestamp:
window.activeTextEditor && previousTimestamp && workspaceElapsedTime
? previousTimestamp
: window.activeTextEditor
? new Date().getTime()
: null,
state: await this._generateDetails(
'lowerDetailsDebugging',
'lowerDetailsEditing',
'lowerDetailsIdle',
largeImageKey,
),
largeImageKey: largeImageKey ? largeImageKey.image || largeImageKey : 'txt',
largeImageText: window.activeTextEditor
? this.client.config
.get<string>('largeImage')!
.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt')
.replace(
'{Lang}',
largeImageKey
? (largeImageKey.image || largeImageKey).toLowerCase().replace(/^\w/, (c: string) => c.toUpperCase())
: 'Txt',
)
.replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT') ||
window.activeTextEditor.document.languageId.padEnd(2, '\u200b')
: this.client.config.get<string>('largeImageIdle'),
smallImageKey: debug.activeDebugSession
? 'debug'
: env.appName.includes('Insiders')
? 'vscode-insiders'
: 'vscode',
smallImageText: this.client.config.get<string>('smallImage')!.replace('{appname}', env.appName),
};
return this._state;
}
public async allowSpectate() {
if (!this._state) return;
const liveshare = await vsls.getApi();
if (!liveshare) return;
const join = await liveshare.share({ suppressNotification: true, access: vsls.Access.ReadOnly });
this._state = {
...this._state,
spectateSecret: join ? Buffer.from(join.toString()).toString('base64') : undefined,
instance: true,
};
return this._state;
}
public async disableSpectate() {
if (!this._state) return;
const liveshare = await vsls.getApi();
if (!liveshare) return;
await liveshare.end();
delete this._state.spectateSecret;
this._state.instance = false;
return this._state;
}
public async allowJoinRequests() {
if (!this._state) return;
const liveshare = await vsls.getApi();
if (!liveshare) return;
const join = await liveshare.share({ suppressNotification: true });
this._state = {
...this._state,
partyId: join ? join.query : undefined,
partySize: 1,
partyMax: 5,
joinSecret: join ? Buffer.from(join.toString()).toString('base64') : undefined,
instance: true,
};
return this._state;
}
public async disableJoinRequests() {
if (!this._state) return;
const liveshare = await vsls.getApi();
if (!liveshare) return;
await liveshare.end();
delete this._state.partyId;
delete this._state.partySize;
delete this._state.partyMax;
delete this._state.joinSecret;
this._state.instance = false;
return this._state;
}
public changePartyId(id?: string) {
if (!this._state) return;
if (!id) {
delete this._state.partyId;
delete this._state.partySize;
delete this._state.partyMax;
this._state.instance = false;
return this._state;
}
this._state = {
...this._state,
partyId: id,
partySize: this._state.partySize ? this._state.partySize + 1 : 1,
partyMax: 5,
instance: true,
};
return this._state;
}
public increasePartySize(size?: number) {
if (!this._state) return;
if (this._state.partySize === 5) return;
this._state = {
...this._state,
partySize: this._state.partySize ? this._state.partySize + 1 : size,
};
return this._state;
}
public decreasePartySize(size?: number) {
if (!this._state) return;
if (this._state.partySize === 1) return;
this._state = {
...this._state,
partySize: this._state.partySize ? this._state.partySize - 1 : size,
};
return this._state;
}
public dispose() {
this._state = null;
this.lastKnownFile = '';
}
private async _generateDetails(debugging: string, editing: string, idling: string, largeImageKey: any) {
let raw = this.client.config.get<string>(idling)!.replace('{null}', empty);
let filename = null;
let dirname = null;
let checkState = false;
let workspaceName = null;
let workspaceFolder = null;
let fullDirname = null;
if (window.activeTextEditor) {
filename = basename(window.activeTextEditor.document.fileName);
const { dir } = parse(window.activeTextEditor.document.fileName);
const split = dir.split(sep);
dirname = split[split.length - 1];
checkState = Boolean(workspace.getWorkspaceFolder(window.activeTextEditor.document.uri));
workspaceName = workspace.name;
workspaceFolder = checkState ? workspace.getWorkspaceFolder(window.activeTextEditor.document.uri) : null;
if (workspaceFolder) {
const { name } = workspaceFolder;
const relativePath = workspace.asRelativePath(window.activeTextEditor.document.fileName).split(sep);
relativePath.splice(-1, 1);
fullDirname = `${name}${sep}${relativePath.join(sep)}`;
}
if (debug.activeDebugSession) {
raw = this.client.config.get<string>(debugging)!;
} else {
raw = this.client.config.get<string>(editing)!;
}
const { totalLines, size, currentLine, currentColumn, gitbranch, gitreponame } = await this._generateFileDetails(
raw,
);
raw = raw
.replace('{null}', empty)
.replace('{filename}', filename)
.replace('{dirname}', dirname)
.replace('{fulldirname}', fullDirname!)
.replace(
'{workspace}',
workspaceName
? workspaceName
: checkState && workspaceFolder
? workspaceFolder.name
: this.client.config.get<string>('lowerDetailsNotFound')!.replace('{null}', empty),
)
.replace(
'{workspaceFolder}',
checkState && workspaceFolder
? workspaceFolder.name
: this.client.config.get<string>('lowerDetailsNotFound')!.replace('{null}', empty),
)
.replace(
'{workspaceAndFolder}',
checkState && workspaceName && workspaceFolder
? `${workspaceName} - ${workspaceFolder.name}`
: this.client.config.get<string>('lowerDetailsNotFound')!.replace('{null}', empty),
)
.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt')
.replace(
'{Lang}',
largeImageKey
? (largeImageKey.image || largeImageKey).toLowerCase().replace(/^\w/, (c: string) => c.toUpperCase())
: 'Txt',
)
.replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT');
if (totalLines) raw = raw.replace('{totallines}', totalLines);
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;
}
private async _generateFileDetails(str?: string) {
const fileDetail: FileDetail = {};
if (!str) return fileDetail;
if (window.activeTextEditor) {
if (str.includes('{totallines}')) {
fileDetail.totalLines = window.activeTextEditor.document.lineCount.toLocaleString();
}
if (str.includes('{currentline}')) {
fileDetail.currentLine = (window.activeTextEditor.selection.active.line + 1).toLocaleString();
}
if (str.includes('{currentcolumn}')) {
fileDetail.currentColumn = (window.activeTextEditor.selection.active.character + 1).toLocaleString();
}
if (str.includes('{filesize}')) {
let currentDivision = 0;
let { size } = await workspace.fs.stat(window.activeTextEditor.document.uri);
const originalSize = size;
if (originalSize > 1000) {
size /= 1000;
currentDivision++;
while (size > 1000) {
currentDivision++;
size /= 1000;
}
}
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;
}
}

View file

@ -1,13 +0,0 @@
import { OutputChannel, window } from 'vscode';
// eslint-disable-next-line
export default class Logger {
private static output?: OutputChannel;
public static log(message: string) {
if (!this.output) {
this.output = window.createOutputChannel('Discord Presence');
}
this.output.appendLine(message);
}
}

56
src/util.ts Normal file
View file

@ -0,0 +1,56 @@
import { basename } from 'path';
import { TextDocument, workspace, WorkspaceConfiguration } from 'vscode';
import { KNOWN_EXTENSIONS, KNOWN_LANGUAGES } from './constants';
type WorkspaceExtensionConfigurationuration = WorkspaceConfiguration & {
enabled: boolean;
details_editing: string;
details_debugging: string;
details_idling: string;
lower_details_editing: string;
lower_details_debugging: string;
lower_details_idling: string;
lower_details_no_workspace_found: string;
large_image: string;
large_image_idling: string;
small_image: string;
suppress_notifications: boolean;
workspace_elapsed_time: boolean;
workspace_exclude_patterns: string[];
};
export function getConfig() {
return workspace.getConfiguration('discord') as WorkspaceExtensionConfigurationuration;
}
export const toLower = (str: string) => str.toLocaleLowerCase();
export const toUpper = (str: string) => str.toLocaleUpperCase();
export const toTitle = (str: string) => toLower(str).replace(/^\w/, (c) => toUpper(c));
export function resolveFileIcon(document: TextDocument) {
const filename = basename(document.fileName);
const findKnownExtension = Object.keys(KNOWN_EXTENSIONS).find((key) => {
if (filename.endsWith(key)) {
return true;
}
const match = /^\/(.*)\/([mgiy]+)$/.exec(key);
if (!match) {
return false;
}
const regex = new RegExp(match[1], match[2]);
return regex.test(filename);
});
const findKnownLanguage = KNOWN_LANGUAGES.find((key) => key.language === document.languageId);
const fileIcon = findKnownExtension
? KNOWN_EXTENSIONS[findKnownExtension]
: findKnownLanguage
? findKnownLanguage.image
: null;
return typeof fileIcon === 'string' ? fileIcon : fileIcon?.image ?? 'text';
}

View file

@ -1,7 +1,7 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
@ -11,7 +11,7 @@
"**/*.test.ts", "**/*.test.ts",
"**/*.test.js", "**/*.test.js",
"**/*.spec.ts", "**/*.spec.ts",
"**/*.spec.js", "**/*.spec.js"
], ],
"exclude": [] "exclude": []
} }

View file

@ -6,11 +6,9 @@
"removeComments": false, "removeComments": false,
"alwaysStrict": true, "alwaysStrict": true,
"pretty": true, "pretty": true,
"target": "es2017", "target": "ES2019",
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": ["ESNext"],
"ESNext"
],
"outDir": "dist", "outDir": "dist",
"sourceMap": true, "sourceMap": true,
"inlineSources": true, "inlineSources": true,

View file

@ -1,7 +1,11 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const path = require('path'); const path = require('path');
/** @type {import('webpack').Configuration} */
module.exports = { module.exports = {
target: 'node', target: 'node',
entry: './src/extension.ts', entry: './src/extension.ts',