format
Some checks failed
Testing / Test (push) Has been cancelled

This commit is contained in:
Michael Zhang 2024-12-08 19:43:18 -06:00
parent 223ae3ea9a
commit 391d8a1beb
12 changed files with 465 additions and 278 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"editor.defaultFormatter": "biomejs.biome"
}

30
biome.json Normal file
View file

@ -0,0 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

BIN
bun.lockb Executable file

Binary file not shown.

View file

@ -19,15 +19,11 @@
"main": "./dist/extension",
"scripts": {
"build": "npm run lint && webpack --mode production",
"lint": "prettier --check . && eslint src --ext mjs,js,ts",
"format": "prettier --write . && eslint src --ext mjs,js,ts --fix"
"lint": "npx @biomejs/biome check",
"format": "npx @biomejs/biome format --write"
},
"activationEvents": [
"*"
],
"extensionKind": [
"ui"
],
"activationEvents": ["*"],
"extensionKind": ["ui"],
"contributes": {
"commands": [
{
@ -165,17 +161,8 @@
"bugs": {
"url": "https://github.com/iCrawl/discord-vscode/issues"
},
"keywords": [
"discord",
"vscode",
"rich",
"presence",
"rich presence",
"rpc"
],
"categories": [
"Other"
],
"keywords": ["discord", "vscode", "rich", "presence", "rich presence", "rpc"],
"categories": ["Other"],
"homepage": "https://github.com/iCrawl/discord-vscode#readme",
"icon": "assets/icon.png",
"galleryBanner": {
@ -191,6 +178,7 @@
"utf-8-validate": "^5.0.9"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/lodash-es": "^4.17.6",
"@types/node": "^17.0.41",
"@types/vscode": "^1.67.0",

View file

@ -1,5 +1,12 @@
import { basename, parse, sep } from 'path';
import { debug, env, Selection, TextDocument, window, workspace } from 'vscode';
import { basename, parse, sep } from "node:path";
import {
type Selection,
type TextDocument,
debug,
env,
window,
workspace,
} from "vscode";
import {
CONFIG_KEYS,
@ -13,9 +20,16 @@ import {
UNKNOWN_GIT_REPO_NAME,
VSCODE_IMAGE_KEY,
VSCODE_INSIDERS_IMAGE_KEY,
} from './constants';
import { log, LogLevel } from './logger';
import { getConfig, getGit, resolveFileIcon, toLower, toTitle, toUpper } from './util';
} from "./constants";
import { LogLevel, log } from "./logger";
import {
getConfig,
getGit,
resolveFileIcon,
toLower,
toTitle,
toUpper,
} from "./util";
interface ActivityPayload {
details?: string | undefined;
@ -35,19 +49,32 @@ interface ActivityPayload {
instance?: boolean | undefined;
}
async function fileDetails(_raw: string, document: TextDocument, selection: Selection) {
async function fileDetails(
_raw: string,
document: TextDocument,
selection: Selection,
) {
let raw = _raw.slice();
if (raw.includes(REPLACE_KEYS.TotalLines)) {
raw = raw.replace(REPLACE_KEYS.TotalLines, document.lineCount.toLocaleString());
raw = raw.replace(
REPLACE_KEYS.TotalLines,
document.lineCount.toLocaleString(),
);
}
if (raw.includes(REPLACE_KEYS.CurrentLine)) {
raw = raw.replace(REPLACE_KEYS.CurrentLine, (selection.active.line + 1).toLocaleString());
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());
raw = raw.replace(
REPLACE_KEYS.CurrentColumn,
(selection.active.character + 1).toLocaleString(),
);
}
if (raw.includes(REPLACE_KEYS.FileSize)) {
@ -70,7 +97,9 @@ async function fileDetails(_raw: string, document: TextDocument, selection: Sele
raw = raw.replace(
REPLACE_KEYS.FileSize,
`${originalSize > 1000 ? size.toFixed(2) : size}${FILE_SIZES[currentDivision]}`,
`${originalSize > 1000 ? size.toFixed(2) : size}${
FILE_SIZES[currentDivision]
}`,
);
}
@ -80,7 +109,8 @@ async function fileDetails(_raw: string, document: TextDocument, selection: Sele
if (git?.repositories.length) {
raw = raw.replace(
REPLACE_KEYS.GitBranch,
git.repositories.find((repo) => repo.ui.selected)?.state.HEAD?.name ?? FAKE_EMPTY,
git.repositories.find((repo) => repo.ui.selected)?.state.HEAD?.name ??
FAKE_EMPTY,
);
} else {
raw = raw.replace(REPLACE_KEYS.GitBranch, UNKNOWN_GIT_BRANCH);
@ -93,8 +123,8 @@ async function fileDetails(_raw: string, document: TextDocument, selection: Sele
REPLACE_KEYS.GitRepoName,
git.repositories
.find((repo) => repo.ui.selected)
?.state.remotes[0].fetchUrl?.split('/')[1]
.replace('.git', '') ?? FAKE_EMPTY,
?.state.remotes[0].fetchUrl?.split("/")[1]
.replace(".git", "") ?? FAKE_EMPTY,
);
} else {
raw = raw.replace(REPLACE_KEYS.GitRepoName, UNKNOWN_GIT_REPO_NAME);
@ -104,7 +134,11 @@ async function fileDetails(_raw: string, document: TextDocument, selection: Sele
return raw;
}
async function details(idling: CONFIG_KEYS, editing: CONFIG_KEYS, debugging: CONFIG_KEYS) {
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, FAKE_EMPTY);
@ -114,12 +148,18 @@ async function details(idling: CONFIG_KEYS, editing: CONFIG_KEYS, debugging: CON
const split = dir.split(sep);
const dirName = split[split.length - 1];
const noWorkspaceFound = config[CONFIG_KEYS.LowerDetailsNoWorkspaceFound].replace(REPLACE_KEYS.Empty, FAKE_EMPTY);
const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri);
const noWorkspaceFound = config[
CONFIG_KEYS.LowerDetailsNoWorkspaceFound
].replace(REPLACE_KEYS.Empty, FAKE_EMPTY);
const workspaceFolder = workspace.getWorkspaceFolder(
window.activeTextEditor.document.uri,
);
const workspaceFolderName = workspaceFolder?.name ?? noWorkspaceFound;
const workspaceName = workspace.name?.replace(REPLACE_KEYS.VSCodeWorkspace, EMPTY) ?? workspaceFolderName;
const workspaceName =
workspace.name?.replace(REPLACE_KEYS.VSCodeWorkspace, EMPTY) ??
workspaceFolderName;
const workspaceAndFolder = `${workspaceName}${
workspaceFolderName === FAKE_EMPTY ? '' : ` - ${workspaceFolderName}`
workspaceFolderName === FAKE_EMPTY ? "" : ` - ${workspaceFolderName}`
}`;
const fileIcon = resolveFileIcon(window.activeTextEditor.document);
@ -132,15 +172,27 @@ async function details(idling: CONFIG_KEYS, editing: CONFIG_KEYS, debugging: CON
if (workspaceFolder) {
const { name } = workspaceFolder;
const relativePath = workspace.asRelativePath(window.activeTextEditor.document.fileName).split(sep);
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 = raw.replace(
REPLACE_KEYS.FullDirName,
`${name}${sep}${relativePath.join(sep)}`,
);
}
try {
raw = await fileDetails(raw, window.activeTextEditor.document, window.activeTextEditor.selection);
raw = await fileDetails(
raw,
window.activeTextEditor.document,
window.activeTextEditor.selection,
);
} catch (error) {
log(LogLevel.Error, `Failed to generate file details: ${error as string}`);
log(
LogLevel.Error,
`Failed to generate file details: ${error as string}`,
);
}
raw = raw
.replace(REPLACE_KEYS.FileName, fileName)
@ -163,10 +215,13 @@ export async function activity(previous: ActivityPayload = {}) {
const appName = env.appName;
const defaultSmallImageKey = debug.activeDebugSession
? DEBUG_IMAGE_KEY
: appName.includes('Insiders')
: appName.includes("Insiders")
? VSCODE_INSIDERS_IMAGE_KEY
: VSCODE_IMAGE_KEY;
const defaultSmallImageText = config[CONFIG_KEYS.SmallImage].replace(REPLACE_KEYS.AppName, appName);
const defaultSmallImageText = config[CONFIG_KEYS.SmallImage].replace(
REPLACE_KEYS.AppName,
appName,
);
const defaultLargeImageText = config[CONFIG_KEYS.LargeImageIdling];
const removeDetails = config[CONFIG_KEYS.RemoveDetails];
const removeLowerDetails = config[CONFIG_KEYS.RemoveLowerDetails];
@ -177,8 +232,14 @@ export async function activity(previous: ActivityPayload = {}) {
let state: ActivityPayload = {
details: removeDetails
? undefined
: await details(CONFIG_KEYS.DetailsIdling, CONFIG_KEYS.DetailsEditing, CONFIG_KEYS.DetailsDebugging),
startTimestamp: config[CONFIG_KEYS.RemoveTimestamp] ? undefined : previous.startTimestamp ?? Date.now(),
: await details(
CONFIG_KEYS.DetailsIdling,
CONFIG_KEYS.DetailsEditing,
CONFIG_KEYS.DetailsDebugging,
),
startTimestamp: config[CONFIG_KEYS.RemoveTimestamp]
? undefined
: (previous.startTimestamp ?? Date.now()),
largeImageKey: IDLE_IMAGE_KEY,
largeImageText: defaultLargeImageText,
smallImageKey: defaultSmallImageKey,
@ -196,18 +257,25 @@ export async function activity(previous: ActivityPayload = {}) {
}
if (!removeRemoteRepository && git?.repositories.length) {
let repo = git.repositories.find((repo) => repo.ui.selected)?.state.remotes[0]?.fetchUrl;
let repo = git.repositories.find((repo) => repo.ui.selected)?.state
.remotes[0]?.fetchUrl;
if (repo) {
if (repo.startsWith('git@') || repo.startsWith('ssh://')) {
repo = repo.replace('ssh://', '').replace(':', '/').replace('git@', 'https://').replace('.git', '');
if (repo.startsWith("git@") || repo.startsWith("ssh://")) {
repo = repo
.replace("ssh://", "")
.replace(":", "/")
.replace("git@", "https://")
.replace(".git", "");
} else {
repo = repo.replace(/(https:\/\/)([^@]*)@(.*?$)/, '$1$3').replace('.git', '');
repo = repo
.replace(/(https:\/\/)([^@]*)@(.*?$)/, "$1$3")
.replace(".git", "");
}
state = {
...state,
buttons: [{ label: 'View Repository', url: repo }],
buttons: [{ label: "View Repository", url: repo }],
};
}
}
@ -224,7 +292,11 @@ export async function activity(previous: ActivityPayload = {}) {
...state,
details: removeDetails
? undefined
: await details(CONFIG_KEYS.DetailsIdling, CONFIG_KEYS.DetailsEditing, CONFIG_KEYS.DetailsDebugging),
: await details(
CONFIG_KEYS.DetailsIdling,
CONFIG_KEYS.DetailsEditing,
CONFIG_KEYS.DetailsDebugging,
),
state: removeLowerDetails
? undefined
: await details(
@ -248,7 +320,10 @@ export async function activity(previous: ActivityPayload = {}) {
};
}
log(LogLevel.Trace, `VSCode language id: ${window.activeTextEditor.document.languageId}`);
log(
LogLevel.Trace,
`VSCode language id: ${window.activeTextEditor.document.languageId}`,
);
}
return state;

View file

@ -1,61 +1,63 @@
import LANG from './data/languages.json';
import LANG from "./data/languages.json";
export const CLIENT_ID = '383226320970055681' as const;
export const CLIENT_ID = "383226320970055681" as const;
export const KNOWN_EXTENSIONS: { [key: string]: { image: string } } = LANG.KNOWN_EXTENSIONS;
export const KNOWN_LANGUAGES: { language: string; image: string }[] = LANG.KNOWN_LANGUAGES;
export const KNOWN_EXTENSIONS: { [key: string]: { image: string } } =
LANG.KNOWN_EXTENSIONS;
export const KNOWN_LANGUAGES: { language: string; image: string }[] =
LANG.KNOWN_LANGUAGES;
export const EMPTY = '' as const;
export const FAKE_EMPTY = '\u200b\u200b' as const;
export const FILE_SIZES = [' bytes', 'KB', 'MB', 'GB', 'TB'] as const;
export const EMPTY = "" as const;
export const FAKE_EMPTY = "\u200b\u200b" as const;
export const FILE_SIZES = [" bytes", "KB", "MB", "GB", "TB"] as const;
export const IDLE_IMAGE_KEY = 'vscode-big' as const;
export const DEBUG_IMAGE_KEY = 'debug' as const;
export const VSCODE_IMAGE_KEY = 'vscode' as const;
export const VSCODE_INSIDERS_IMAGE_KEY = 'vscode-insiders' as const;
export const IDLE_IMAGE_KEY = "vscode-big" as const;
export const DEBUG_IMAGE_KEY = "debug" as const;
export const VSCODE_IMAGE_KEY = "vscode" as const;
export const VSCODE_INSIDERS_IMAGE_KEY = "vscode-insiders" as const;
export const UNKNOWN_GIT_BRANCH = 'Unknown' as const;
export const UNKNOWN_GIT_REPO_NAME = 'Unknown' as const;
export const UNKNOWN_GIT_BRANCH = "Unknown" as const;
export const UNKNOWN_GIT_REPO_NAME = "Unknown" as const;
export const enum REPLACE_KEYS {
Empty = '{empty}',
FileName = '{file_name}',
DirName = '{dir_name}',
FullDirName = '{full_dir_name}',
Workspace = '{workspace}',
VSCodeWorkspace = '(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 enum REPLACE_KEYS {
Empty = "{empty}",
FileName = "{file_name}",
DirName = "{dir_name}",
FullDirName = "{full_dir_name}",
Workspace = "{workspace}",
VSCodeWorkspace = "(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 = 'detailsIdling',
DetailsEditing = 'detailsEditing',
DetailsDebugging = 'detailsDebugging',
LowerDetailsIdling = 'lowerDetailsIdling',
LowerDetailsEditing = 'lowerDetailsEditing',
LowerDetailsDebugging = 'lowerDetailsDebugging',
LowerDetailsNoWorkspaceFound = 'lowerDetailsNoWorkspaceFound',
LargeImageIdling = 'largeImageIdling',
LargeImage = 'largeImage',
SmallImage = 'smallImage',
SuppressNotifications = 'suppressNotifications',
WorkspaceExcludePatterns = 'workspaceExcludePatterns',
SwapBigAndSmallImage = 'swapBigAndSmallImage',
RemoveDetails = 'removeDetails',
RemoveLowerDetails = 'removeLowerDetails',
RemoveTimestamp = 'removeTimestamp',
RemoveRemoteRepository = 'removeRemoteRepository',
IdleTimeout = 'idleTimeout',
export enum CONFIG_KEYS {
Enabled = "enabled",
DetailsIdling = "detailsIdling",
DetailsEditing = "detailsEditing",
DetailsDebugging = "detailsDebugging",
LowerDetailsIdling = "lowerDetailsIdling",
LowerDetailsEditing = "lowerDetailsEditing",
LowerDetailsDebugging = "lowerDetailsDebugging",
LowerDetailsNoWorkspaceFound = "lowerDetailsNoWorkspaceFound",
LargeImageIdling = "largeImageIdling",
LargeImage = "largeImage",
SmallImage = "smallImage",
SuppressNotifications = "suppressNotifications",
WorkspaceExcludePatterns = "workspaceExcludePatterns",
SwapBigAndSmallImage = "swapBigAndSmallImage",
RemoveDetails = "removeDetails",
RemoveLowerDetails = "removeLowerDetails",
RemoveTimestamp = "removeTimestamp",
RemoveRemoteRepository = "removeRemoteRepository",
IdleTimeout = "idleTimeout",
}

View file

@ -82,11 +82,15 @@
".asmx": { "image": "asp" },
".aspx": { "image": "asp" },
".axd": { "image": "asp" },
"/\\.(l?a|[ls]?o|out|s|a51|asm|axf|elf|prx|puff|z80)$/i": { "image": "assembly" },
"/\\.(l?a|[ls]?o|out|s|a51|asm|axf|elf|prx|puff|z80)$/i": {
"image": "assembly"
},
".agc": { "image": "assembly" },
".ko": { "image": "assembly" },
".lst": { "image": "assembly" },
"/\\.((c([+px]{2}?)?-?)?objdump|bsdiff|bin|dat|pak|pdb)$/i": { "image": "assembly" },
"/\\.((c([+px]{2}?)?-?)?objdump|bsdiff|bin|dat|pak|pdb)$/i": {
"image": "assembly"
},
".d-objdump": { "image": "assembly" },
"/\\.gcode|\\.gco/i": { "image": "assembly" },
"/\\.rpy[bc]$/i": { "image": "assembly" },
@ -150,7 +154,9 @@
".dm": { "image": "dm" },
".dme": { "image": "dm" },
".dmm": { "image": "dm" },
"/^(Dockerfile|docker-compose)|\\.docker(file|ignore)$/i": { "image": "docker" },
"/^(Dockerfile|docker-compose)|\\.docker(file|ignore)$/i": {
"image": "docker"
},
"/^docker-sync\\.yml$/i": { "image": "docker" },
".editorconfig": { "image": "editorconfig" },
".ejs": { "image": "ejs" },
@ -274,13 +280,19 @@
"/^pkginfo$/": { "image": "manifest" },
"/^mime\\.types$/i": { "image": "manifest" },
"/^METADATA\\.pb$/": { "image": "manifest" },
"/[\\/\\\\](?:magic[\\/\\\\]Magdir|file[\\/\\\\]magic)[\\/\\\\][-.\\w]+$/i": { "image": "manifest" },
"/(\\\\|\\/)dev[-\\w]+\\1(?:[^\\\\\\/]+\\1)*(?!DESC|NOTES)(?:[A-Z][-A-Z]*)(?:\\.in)?$/": { "image": "manifest" },
"/[\\/\\\\](?:magic[\\/\\\\]Magdir|file[\\/\\\\]magic)[\\/\\\\][-.\\w]+$/i": {
"image": "manifest"
},
"/(\\\\|\\/)dev[-\\w]+\\1(?:[^\\\\\\/]+\\1)*(?!DESC|NOTES)(?:[A-Z][-A-Z]*)(?:\\.in)?$/": {
"image": "manifest"
},
"lib/icons/.icondb.js": { "image": "manifest" },
"/\\.git[\\/\\\\](.*[\\/\\\\])?(HEAD|ORIG_HEAD|packed-refs|logs[\\/\\\\](.+[\\/\\\\])?[^\\/\\\\]+)$/": {
"image": "manifest"
},
"/\\.(md|mdown|markdown|mkd|mkdown|mdwn|mkdn|rmd|ron|pmd)$/i": { "image": "markdown" },
"/\\.(md|mdown|markdown|mkd|mkdown|mdwn|mkdn|rmd|ron|pmd)$/i": {
"image": "markdown"
},
".mdx": { "image": "markdownx" },
".marko": { "image": "marko" },
".nim": { "image": "nim" },
@ -329,7 +341,9 @@
".psm1": { "image": "powershell" },
".ps1xml": { "image": "powershell" },
".prettierignore": { "image": "prettier" },
"/\\.prettier((rc)|(\\.(toml|yml|yaml|json|js))?$){2}/i": { "image": "prettier" },
"/\\.prettier((rc)|(\\.(toml|yml|yaml|json|js))?$){2}/i": {
"image": "prettier"
},
"prettier.config.js": { "image": "prettier" },
"prisma.yml": { "image": "prisma" },
".pde": { "image": "processing" },
@ -359,7 +373,9 @@
"/\\.(r|Rprofile|rsx|rd)$/i": { "image": "r" },
".rkt": { "image": "racket" },
"/\\.res?i?$/i": { "image": "reasonml" },
"/\\.(rb|ru|ruby|erb|gemspec|god|mspec|pluginspec|podspec|rabl|rake|opal)$/i": { "image": "ruby" },
"/\\.(rb|ru|ruby|erb|gemspec|god|mspec|pluginspec|podspec|rabl|rake|opal)$/i": {
"image": "ruby"
},
"/^\\.?(irbrc|gemrc|pryrc|ruby-(gemset|version))$/i": { "image": "ruby" },
"/^(Appraisals|(Rake|[bB]uild|Cap|Danger|Deliver|Fast|Guard|Jar|Maven|Pod|Puppet|Snap)file(\\.lock)?)$/": {
"image": "ruby"
@ -377,13 +393,19 @@
},
"/\\.(ksh|mksh|pdksh)$/i": { "image": "shell" },
".sh-session": { "image": "shell" },
"/\\.zsh(-theme|_history)?$|^\\.?(antigen|zpreztorc|zlogin|zlogout|zprofile|zshenv|zshrc)$/i": { "image": "shell" },
"/\\.zsh(-theme|_history)?$|^\\.?(antigen|zpreztorc|zlogin|zlogout|zprofile|zshenv|zshrc)$/i": {
"image": "shell"
},
"/\\.fish$|^\\.fishrc$/i": { "image": "shell" },
"/^\\.?(login|profile)$/": { "image": "shell" },
".inputrc": { "image": "shell" },
".tmux": { "image": "shell" },
"/^(configure|config\\.(guess|rpath|status|sub)|depcomp|libtool|compile)$/": { "image": "shell" },
"/^\\/(private\\/)?etc\\/([^\\/]+\\/)*(profile$|nanorc$|rc\\.|csh\\.)/i": { "image": "shell" },
"/^(configure|config\\.(guess|rpath|status|sub)|depcomp|libtool|compile)$/": {
"image": "shell"
},
"/^\\/(private\\/)?etc\\/([^\\/]+\\/)*(profile$|nanorc$|rc\\.|csh\\.)/i": {
"image": "shell"
},
"/^\\.?cshrc$/i": { "image": "shell" },
".profile": { "image": "shell" },
".tcsh": { "image": "shell" },

View file

@ -1,28 +1,40 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
const { Client } = require('discord-rpc'); // eslint-disable-line
import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, window, workspace, debug } from 'vscode';
import throttle from 'lodash-es/throttle';
const { Client } = require("discord-rpc"); // eslint-disable-line
import throttle from "lodash-es/throttle";
import {
type ExtensionContext,
StatusBarAlignment,
type StatusBarItem,
commands,
debug,
window,
workspace,
} from "vscode";
import { activity } from './activity';
import { CLIENT_ID, CONFIG_KEYS } from './constants';
import { log, LogLevel } from './logger';
import { getConfig, getGit } from './util';
import { activity } from "./activity";
import { CLIENT_ID, CONFIG_KEYS } from "./constants";
import { LogLevel, log } from "./logger";
import { getConfig, getGit } from "./util";
const statusBarIcon: StatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
statusBarIcon.text = '$(pulse) Connecting to Discord...';
const statusBarIcon: StatusBarItem = window.createStatusBarItem(
StatusBarAlignment.Left,
);
statusBarIcon.text = "$(pulse) Connecting to Discord...";
// eslint-disable-next-line
let rpc = new Client({ transport: 'ipc' });
let rpc = new Client({ transport: "ipc" });
const config = getConfig();
let state = {};
let idle: NodeJS.Timeout | undefined;
let listeners: { dispose: () => any }[] = [];
let listeners: { dispose: () => void }[] = [];
export function cleanUp() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
listeners.forEach((listener) => listener.dispose());
for (const listener of listeners) {
listener.dispose();
}
listeners = [];
}
@ -34,50 +46,70 @@ async function sendActivity() {
}
async function login() {
log(LogLevel.Info, 'Creating discord-rpc client');
rpc = new Client({ transport: 'ipc' });
log(LogLevel.Info, "Creating discord-rpc client");
rpc = new Client({ transport: "ipc" });
rpc.on('ready', () => {
log(LogLevel.Info, 'Successfully connected to Discord');
rpc.on("ready", () => {
log(LogLevel.Info, "Successfully connected to Discord");
cleanUp();
statusBarIcon.text = '$(globe) Connected to Discord';
statusBarIcon.tooltip = '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(throttle(() => sendActivity(), 2000));
const onStartDebugSession = debug.onDidStartDebugSession(() => sendActivity());
const onTerminateDebugSession = debug.onDidTerminateDebugSession(() => sendActivity());
const onChangeActiveTextEditor = window.onDidChangeActiveTextEditor(() =>
sendActivity(),
);
const onChangeTextDocument = workspace.onDidChangeTextDocument(
throttle(() => sendActivity(), 2000),
);
const onStartDebugSession = debug.onDidStartDebugSession(() =>
sendActivity(),
);
const onTerminateDebugSession = debug.onDidTerminateDebugSession(() =>
sendActivity(),
);
listeners.push(onChangeActiveTextEditor, onChangeTextDocument, onStartDebugSession, onTerminateDebugSession);
listeners.push(
onChangeActiveTextEditor,
onChangeTextDocument,
onStartDebugSession,
onTerminateDebugSession,
);
});
rpc.on('disconnected', () => {
rpc.on("disconnected", () => {
cleanUp();
rpc.destroy();
statusBarIcon.text = '$(pulse) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect';
statusBarIcon.text = "$(pulse) Reconnect to Discord";
statusBarIcon.command = "discord.reconnect";
});
try {
await rpc.login({ clientId: CLIENT_ID });
} catch (error) {
log(LogLevel.Error, `Encountered following error while trying to login:\n${error as string}`);
log(
LogLevel.Error,
`Encountered following error while trying to login:\n${error as string}`,
);
cleanUp();
rpc.destroy();
if (!config[CONFIG_KEYS.SuppressNotifications]) {
// @ts-expect-error
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}`);
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}`,
);
}
statusBarIcon.text = '$(pulse) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect';
statusBarIcon.text = "$(pulse) Reconnect to Discord";
statusBarIcon.command = "discord.reconnect";
}
}
export async function activate(context: ExtensionContext) {
log(LogLevel.Info, 'Discord Presence activated');
log(LogLevel.Info, "Discord Presence activated");
let isWorkspaceExcluded = false;
for (const pattern of config[CONFIG_KEYS.WorkspaceExcludePatterns]) {
@ -93,52 +125,62 @@ export async function activate(context: ExtensionContext) {
const enable = async (update = true) => {
if (update) {
try {
await config.update('enabled', true);
await config.update("enabled", true);
} catch {}
}
log(LogLevel.Info, 'Enable: Cleaning up old listeners');
log(LogLevel.Info, "Enable: Cleaning up old listeners");
cleanUp();
statusBarIcon.text = '$(pulse) Connecting to Discord...';
statusBarIcon.text = "$(pulse) Connecting to Discord...";
statusBarIcon.show();
log(LogLevel.Info, 'Enable: Attempting to recreate login');
log(LogLevel.Info, "Enable: Attempting to recreate login");
void login();
};
const disable = async (update = true) => {
if (update) {
try {
await config.update('enabled', false);
await config.update("enabled", false);
} catch {}
}
log(LogLevel.Info, 'Disable: Cleaning up old listeners');
log(LogLevel.Info, "Disable: Cleaning up old listeners");
cleanUp();
void rpc?.destroy();
log(LogLevel.Info, 'Disable: Destroyed the rpc instance');
log(LogLevel.Info, "Disable: Destroyed the rpc instance");
statusBarIcon.hide();
};
const enabler = commands.registerCommand('discord.enable', async () => {
const enabler = commands.registerCommand("discord.enable", async () => {
await disable();
await enable();
await window.showInformationMessage('Enabled Discord Presence for this workspace');
await window.showInformationMessage(
"Enabled Discord Presence for this workspace",
);
});
const disabler = commands.registerCommand('discord.disable', async () => {
const disabler = commands.registerCommand("discord.disable", async () => {
await disable();
await window.showInformationMessage('Disabled Discord Presence for this workspace');
await window.showInformationMessage(
"Disabled Discord Presence for this workspace",
);
});
const reconnecter = commands.registerCommand('discord.reconnect', async () => {
const reconnecter = commands.registerCommand(
"discord.reconnect",
async () => {
await disable(false);
await enable(false);
});
},
);
const disconnect = commands.registerCommand('discord.disconnect', async () => {
const disconnect = commands.registerCommand(
"discord.disconnect",
async () => {
await disable(false);
statusBarIcon.text = '$(pulse) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect';
statusBarIcon.text = "$(pulse) Reconnect to Discord";
statusBarIcon.command = "discord.reconnect";
statusBarIcon.show();
});
},
);
context.subscriptions.push(enabler, disabler, reconnecter, disconnect);

146
src/git.d.ts vendored
View file

@ -12,8 +12,8 @@
* --------------------------------------------------------------------------------------------
*/
import { Uri, Event, Disposable, ProviderResult } from 'vscode';
export { ProviderResult } from 'vscode';
import type { Disposable, Event, ProviderResult, Uri } from "vscode";
export { ProviderResult } from "vscode";
export interface Git {
readonly path: string;
@ -23,15 +23,15 @@ export interface InputBox {
value: string;
}
export const enum ForcePushMode {
Force,
ForceWithLease,
export enum ForcePushMode {
Force = "Force",
ForceWithLease = "ForceWithLease",
}
export const enum RefType {
Head,
RemoteHead,
Tag,
export enum RefType {
Head = "Head",
RemoteHead = "RemoteHead",
Tag = "Tag",
}
export interface Ref {
@ -75,26 +75,26 @@ export interface Remote {
readonly isReadOnly: boolean;
}
export const enum Status {
INDEX_MODIFIED,
INDEX_ADDED,
INDEX_DELETED,
INDEX_RENAMED,
INDEX_COPIED,
export enum Status {
INDEX_MODIFIED = "INDEX_MODIFIED",
INDEX_ADDED = "INDEX_ADDED",
INDEX_DELETED = "INDEX_DELETED",
INDEX_RENAMED = "INDEX_RENAMED",
INDEX_COPIED = "INDEX_COPIED",
MODIFIED,
DELETED,
UNTRACKED,
IGNORED,
INTENT_TO_ADD,
MODIFIED = "MODIFIED",
DELETED = "DELETED",
UNTRACKED = "UNTRACKED",
IGNORED = "IGNORED",
INTENT_TO_ADD = "INTENT_TO_ADD",
ADDED_BY_US,
ADDED_BY_THEM,
DELETED_BY_US,
DELETED_BY_THEM,
BOTH_ADDED,
BOTH_DELETED,
BOTH_MODIFIED,
ADDED_BY_US = "ADDED_BY_US",
ADDED_BY_THEM = "ADDED_BY_THEM",
DELETED_BY_US = "DELETED_BY_US",
DELETED_BY_THEM = "DELETED_BY_THEM",
BOTH_ADDED = "BOTH_ADDED",
BOTH_DELETED = "BOTH_DELETED",
BOTH_MODIFIED = "BOTH_MODIFIED",
}
export interface Change {
@ -138,7 +138,7 @@ export interface LogOptions {
}
export interface CommitOptions {
all?: boolean | 'tracked';
all?: boolean | "tracked";
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
@ -173,8 +173,13 @@ export interface Repository {
setConfig(key: string, value: string): Promise<string>;
getGlobalConfig(key: string): Promise<string>;
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }>;
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }>;
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<Buffer>;
show(ref: string, path: string): Promise<string>;
getCommit(ref: string): Promise<Commit>;
@ -215,7 +220,12 @@ export interface Repository {
fetch(options?: FetchOptions): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise<void>;
push(
remoteName?: string,
branchName?: string,
setUpstream?: boolean,
force?: ForcePushMode,
): Promise<void>;
blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>;
@ -256,7 +266,7 @@ export interface PushErrorHandler {
): Promise<boolean>;
}
export type APIState = 'uninitialized' | 'initialized';
export type APIState = "uninitialized" | "initialized";
export interface PublishEvent {
repository: Repository;
@ -299,40 +309,40 @@ export interface GitExtension {
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',
PermissionDenied = 'PermissionDenied',
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',
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
export 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",
PermissionDenied = "PermissionDenied",
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",
PatchDoesNotApply = "PatchDoesNotApply",
NoPathFound = "NoPathFound",
UnknownPath = "UnknownPath",
}

View file

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

View file

@ -1,9 +1,14 @@
import { basename } from 'path';
import { TextDocument, workspace, extensions, WorkspaceConfiguration } from 'vscode';
import { basename } from "node:path";
import {
type TextDocument,
type WorkspaceConfiguration,
extensions,
workspace,
} from "vscode";
import { KNOWN_EXTENSIONS, KNOWN_LANGUAGES } from './constants';
import type { API, GitExtension } from './git';
import { log, LogLevel } from './logger';
import { KNOWN_EXTENSIONS, KNOWN_LANGUAGES } from "./constants";
import type { API, GitExtension } from "./git";
import { LogLevel, log } from "./logger";
let git: API | null | undefined;
@ -30,14 +35,17 @@ type WorkspaceExtensionConfiguration = WorkspaceConfiguration & {
};
export function getConfig() {
return workspace.getConfiguration('discord') as WorkspaceExtensionConfiguration;
return workspace.getConfiguration(
"discord",
) as WorkspaceExtensionConfiguration;
}
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 const toTitle = (str: string) =>
toLower(str).replace(/^\w/, (c) => toUpper(c));
export function resolveFileIcon(document: TextDocument) {
const filename = basename(document.fileName);
@ -54,14 +62,16 @@ export function resolveFileIcon(document: TextDocument) {
const regex = new RegExp(match[1], match[2]);
return regex.test(filename);
});
const findKnownLanguage = KNOWN_LANGUAGES.find((key) => key.language === document.languageId);
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';
return typeof fileIcon === "string" ? fileIcon : (fileIcon?.image ?? "text");
}
export async function getGit() {
@ -70,16 +80,19 @@ export async function getGit() {
}
try {
log(LogLevel.Debug, 'Loading git extension');
const gitExtension = extensions.getExtension<GitExtension>('vscode.git');
log(LogLevel.Debug, "Loading git extension");
const gitExtension = extensions.getExtension<GitExtension>("vscode.git");
if (!gitExtension?.isActive) {
log(LogLevel.Trace, 'Git extension not activated, activating...');
log(LogLevel.Trace, "Git extension not activated, activating...");
await gitExtension?.activate();
}
git = gitExtension?.exports.getAPI(1);
} catch (error) {
git = null;
log(LogLevel.Error, `Failed to load git extension, is git installed?; ${error as string}`);
log(
LogLevel.Error,
`Failed to load git extension, is git installed?; ${error as string}`,
);
}
return git;

View file

@ -1,25 +1,25 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const path = require("node:path");
/** @type {import('webpack').Configuration} */
module.exports = {
target: 'node',
entry: './src/extension.ts',
target: "node",
entry: "./src/extension.ts",
output: {
filename: 'extension.js',
libraryTarget: 'commonjs2',
path: path.resolve(process.cwd(), 'dist'),
filename: "extension.js",
libraryTarget: "commonjs2",
path: path.resolve(process.cwd(), "dist"),
},
devtool: 'source-map',
devtool: "source-map",
externals: {
vscode: 'commonjs vscode',
vscode: "commonjs vscode",
},
resolve: {
extensions: ['.ts', '.js', '.json'],
extensions: [".ts", ".js", ".json"],
},
plugins: [new CleanWebpackPlugin()],
optimization: {
@ -42,7 +42,7 @@ module.exports = {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
use: "ts-loader",
exclude: /node_modules/,
},
],