mirror of
https://github.com/ckaczor/vscode-gitlens.git
synced 2026-01-14 01:25:43 -05:00
254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
import { Dates, Strings } from '../system';
|
|
import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode';
|
|
import { DiffWithCommand, OpenCommitInRemoteCommand, ShowQuickCommitDetailsCommand } from '../commands';
|
|
import { IThemeConfig, themeDefaults } from '../configuration';
|
|
import { GlyphChars } from '../constants';
|
|
import { CommitFormatter, GitCommit, GitDiffChunkLine, GitService, GitUri, ICommitFormatOptions } from '../gitService';
|
|
const Datauri = require('datauri');
|
|
|
|
interface IHeatmapConfig {
|
|
enabled: boolean;
|
|
location?: 'left' | 'right';
|
|
}
|
|
|
|
interface IRenderOptions {
|
|
uncommittedForegroundColor?: {
|
|
dark: string;
|
|
light: string;
|
|
};
|
|
|
|
before?: DecorationInstanceRenderOptions & ThemableDecorationRenderOptions & { height?: string };
|
|
dark?: DecorationInstanceRenderOptions;
|
|
light?: DecorationInstanceRenderOptions;
|
|
}
|
|
|
|
export const endOfLineIndex = 1000000;
|
|
const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g;
|
|
// const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___';
|
|
|
|
const linkIconSvg = `<?xml version="1.0" encoding="utf-8"?>
|
|
<svg width="12" height="18" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill="\${color}" d="m11,14l1,0l0,3c0,0.55 -0.45,1 -1,1l-10,0c-0.55,0 -1,-0.45 -1,-1l0,-10c0,-0.55 0.45,-1 1,-1l3,0l0,1l-3,0l0,10l10,0l0,-3l0,0zm-5,-8l2.25,2.25l-3.25,3.25l1.5,1.5l3.25,-3.25l2.25,2.25l0,-6l-6,0l0,0z" />
|
|
</svg>`;
|
|
|
|
const themeForegroundColor = '#a0a0a0';
|
|
let linkIconDataUri: string | undefined;
|
|
|
|
function getLinkIconDataUri(foregroundColor: string): string {
|
|
if (linkIconDataUri === undefined || foregroundColor !== themeForegroundColor) {
|
|
const datauri = new Datauri();
|
|
datauri.format('.svg', Strings.interpolate(linkIconSvg, { color: foregroundColor }));
|
|
linkIconDataUri = datauri.content;
|
|
foregroundColor = themeForegroundColor;
|
|
}
|
|
|
|
return linkIconDataUri!;
|
|
}
|
|
|
|
export class Annotations {
|
|
|
|
static applyHeatmap(decoration: DecorationOptions, date: Date, now: number) {
|
|
const color = this._getHeatmapColor(now, date);
|
|
(decoration.renderOptions!.before! as any).borderColor = color;
|
|
}
|
|
|
|
private static _getHeatmapColor(now: number, date: Date) {
|
|
const days = Dates.dateDaysFromNow(date, now);
|
|
|
|
if (days <= 2) return '#ffeca7';
|
|
if (days <= 7) return '#ffdd8c';
|
|
if (days <= 14) return '#ffdd7c';
|
|
if (days <= 30) return '#fba447';
|
|
if (days <= 60) return '#f68736';
|
|
if (days <= 90) return '#f37636';
|
|
if (days <= 180) return '#ca6632';
|
|
if (days <= 365) return '#c0513f';
|
|
if (days <= 730) return '#a2503a';
|
|
return '#793738';
|
|
}
|
|
|
|
static getHoverMessage(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): MarkdownString {
|
|
if (dateFormat === null) {
|
|
dateFormat = 'MMMM Do, YYYY h:MMa';
|
|
}
|
|
|
|
let message = '';
|
|
if (!commit.isUncommitted) {
|
|
message = commit.message
|
|
// Escape markdown
|
|
.replace(escapeMarkdownRegEx, '\\$&')
|
|
// Escape markdown header (since the above regex won't match it)
|
|
.replace(/^===/gm, `${GlyphChars.ZeroWidthSpace}===`)
|
|
// Keep under the same block-quote
|
|
.replace(/\n/g, ' \n');
|
|
message = `\n\n> ${message}`;
|
|
}
|
|
|
|
const openInRemoteCommand = hasRemotes
|
|
? `${' '.repeat(3)} [})](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(commit.sha)} "Open in Remote")`
|
|
: '';
|
|
|
|
const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details") __${commit.author}__, ${commit.fromNow()} _(${commit.formatDate(dateFormat)})_ ${openInRemoteCommand} ${message}`);
|
|
markdown.isTrusted = true;
|
|
return markdown;
|
|
}
|
|
|
|
static getHoverDiffMessage(commit: GitCommit, chunkLine: GitDiffChunkLine | undefined): MarkdownString | undefined {
|
|
if (chunkLine === undefined) return undefined;
|
|
|
|
const codeDiff = this._getCodeDiff(chunkLine);
|
|
const markdown = new MarkdownString(commit.isUncommitted
|
|
? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") ${GlyphChars.Dash} _uncommitted_\n${codeDiff}`
|
|
: `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") ${GlyphChars.Dash} [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)} "Show Commit Details") ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details")\n${codeDiff}`);
|
|
markdown.isTrusted = true;
|
|
return markdown;
|
|
}
|
|
|
|
private static _getCodeDiff(chunkLine: GitDiffChunkLine): string {
|
|
const previous = chunkLine.previous === undefined ? undefined : chunkLine.previous[0];
|
|
return `\`\`\`
|
|
- ${previous === undefined || previous.line === undefined ? '' : previous.line.trim()}
|
|
+ ${chunkLine.line === undefined ? '' : chunkLine.line.trim()}
|
|
\`\`\``;
|
|
}
|
|
|
|
static async changesHover(commit: GitCommit, line: number, uri: GitUri, git: GitService): Promise<DecorationOptions> {
|
|
const chunkLine = await git.getDiffForLine(uri, line + uri.offset, commit.isUncommitted ? undefined : commit.previousSha);
|
|
const message = this.getHoverDiffMessage(commit, chunkLine);
|
|
|
|
return {
|
|
hoverMessage: message
|
|
} as DecorationOptions;
|
|
}
|
|
|
|
static detailsHover(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): DecorationOptions {
|
|
const message = this.getHoverMessage(commit, dateFormat, hasRemotes);
|
|
return {
|
|
hoverMessage: message
|
|
} as DecorationOptions;
|
|
}
|
|
|
|
static gutter(commit: GitCommit, format: string, dateFormatOrFormatOptions: string | null | ICommitFormatOptions, renderOptions: IRenderOptions): DecorationOptions {
|
|
const content = Strings.pad(CommitFormatter.fromTemplate(format, commit, dateFormatOrFormatOptions), 1, 1);
|
|
|
|
return {
|
|
renderOptions: {
|
|
before: {
|
|
...renderOptions.before,
|
|
...{
|
|
contentText: content
|
|
}
|
|
},
|
|
dark: {
|
|
before: commit.isUncommitted
|
|
? { ...renderOptions.dark, ...{ color: renderOptions.uncommittedForegroundColor!.dark } }
|
|
: { ...renderOptions.dark }
|
|
},
|
|
light: {
|
|
before: commit.isUncommitted
|
|
? { ...renderOptions.light, ...{ color: renderOptions.uncommittedForegroundColor!.light } }
|
|
: { ...renderOptions.light }
|
|
}
|
|
} as DecorationInstanceRenderOptions
|
|
} as DecorationOptions;
|
|
}
|
|
|
|
static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
|
|
const cfgFileTheme = cfgTheme.annotations.file.gutter;
|
|
|
|
let borderStyle = undefined;
|
|
let borderWidth = undefined;
|
|
if (heatmap.enabled) {
|
|
borderStyle = 'solid';
|
|
borderWidth = heatmap.location === 'left' ? '0 0 0 2px' : '0 2px 0 0';
|
|
}
|
|
|
|
return {
|
|
uncommittedForegroundColor: {
|
|
dark: cfgFileTheme.dark.uncommittedForegroundColor || cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor,
|
|
light: cfgFileTheme.light.uncommittedForegroundColor || cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor
|
|
},
|
|
before: {
|
|
borderStyle: borderStyle,
|
|
borderWidth: borderWidth,
|
|
height: '100%',
|
|
margin: '0 26px -1px 0'
|
|
},
|
|
dark: {
|
|
backgroundColor: cfgFileTheme.dark.backgroundColor || undefined,
|
|
color: cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor,
|
|
textDecoration: cfgFileTheme.separateLines ? 'overline solid rgba(0, 0, 0, .2)' : 'none'
|
|
} as DecorationInstanceRenderOptions,
|
|
light: {
|
|
backgroundColor: cfgFileTheme.light.backgroundColor || undefined,
|
|
color: cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor,
|
|
textDecoration: cfgFileTheme.separateLines ? 'overline solid rgba(0, 0, 0, .05)' : 'none'
|
|
} as DecorationInstanceRenderOptions
|
|
} as IRenderOptions;
|
|
}
|
|
|
|
static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean, dateFormat: string | null, hasRemotes: boolean): DecorationOptions {
|
|
return {
|
|
hoverMessage: this.getHoverMessage(commit, dateFormat, hasRemotes),
|
|
renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined
|
|
} as DecorationOptions;
|
|
}
|
|
|
|
static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
|
|
if (!heatmap.enabled) return { before: undefined };
|
|
|
|
return {
|
|
before: {
|
|
borderStyle: 'solid',
|
|
borderWidth: '0 0 0 2px',
|
|
contentText: GlyphChars.ZeroWidthSpace,
|
|
height: '100%',
|
|
margin: '0 26px 0 0',
|
|
textDecoration: 'none'
|
|
}
|
|
} as IRenderOptions;
|
|
}
|
|
|
|
static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions {
|
|
const message = CommitFormatter.fromTemplate(format, commit, {
|
|
truncateMessageAtNewLine: true,
|
|
dateFormat: dateFormat
|
|
} as ICommitFormatOptions);
|
|
return {
|
|
renderOptions: {
|
|
after: {
|
|
contentText: Strings.pad(message, 1, 1)
|
|
},
|
|
dark: {
|
|
after: {
|
|
backgroundColor: cfgTheme.annotations.line.trailing.dark.backgroundColor || undefined,
|
|
color: cfgTheme.annotations.line.trailing.dark.foregroundColor || themeDefaults.annotations.line.trailing.dark.foregroundColor
|
|
}
|
|
},
|
|
light: {
|
|
after: {
|
|
backgroundColor: cfgTheme.annotations.line.trailing.light.backgroundColor || undefined,
|
|
color: cfgTheme.annotations.line.trailing.light.foregroundColor || themeDefaults.annotations.line.trailing.light.foregroundColor
|
|
}
|
|
}
|
|
} as DecorationInstanceRenderOptions
|
|
} as DecorationOptions;
|
|
}
|
|
|
|
static withRange(decoration: DecorationOptions, start?: number, end?: number): DecorationOptions {
|
|
let range = decoration.range;
|
|
if (start !== undefined) {
|
|
range = range.with({
|
|
start: range.start.with({ character: start })
|
|
});
|
|
}
|
|
|
|
if (end !== undefined) {
|
|
range = range.with({
|
|
end: range.end.with({ character: end })
|
|
});
|
|
}
|
|
|
|
return { ...decoration, ...{ range: range } };
|
|
}
|
|
} |