diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f974de..ae4df5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [5.0.1] - 2017-09-14 ### Added +- Adds an external link icon to the `details` hover annotation to run the `Open Commit in Remote` command (`gitlens.openCommitInRemote`) ### Changed -- Optimizes date handling (parsing and formatting) to increase blame annotation performance and reduce memory consumption +- Optimizes performance of the providing blame annotations, especially for large files (saw a ~61% improvement on some files) +- Optimizes date handling (parsing and formatting) for better performance and reduced memory consumption ### Fixed diff --git a/README.md b/README.md index ac9b5e3..d727828 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, - Adds a `changes` (diff) hover annotation to the current line annotation, which provides **instant** access to the line's previous version ([optional](#line-blame-annotation-settings), on by default) - Clicking on `Changes` will run the `Compare File Revisions` command (`gitlens.diffWith`) - Clicking the current and previous commit ids will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`) + - Clicking on external link icon will run the the `Open Commit in Remote` command (`gitlens.openCommitInRemote`) ![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png) diff --git a/package-lock.json b/package-lock.json index 3bfdc91..6770591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -384,6 +384,16 @@ } } }, + "datauri": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/datauri/-/datauri-1.0.5.tgz", + "integrity": "sha1-0JddGrbI8uDOPKQ7qkU5vhLSiaA=", + "requires": { + "image-size": "0.3.5", + "mimer": "0.2.1", + "semver": "5.4.1" + } + }, "dateformat": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz", @@ -1281,6 +1291,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.5.tgz", "integrity": "sha512-JLH93mL8amZQhh/p6mfQgVBH3M6epNq3DfsXsTSuSrInVjwyYlFE1nv2AgfRCC8PoOhM0jwQ5v8s9LgbK7yGDw==" }, + "image-size": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.3.5.tgz", + "integrity": "sha1-gyQOqy+1sAsEqrjHSwRx6cunrYw=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1769,6 +1784,11 @@ "mime-db": "1.30.0" } }, + "mimer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/mimer/-/mimer-0.2.1.tgz", + "integrity": "sha1-xjxaF/6GQj9RYahdVcPtUYm6r/w=" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", diff --git a/package.json b/package.json index f5b7150..2fc0d7a 100644 --- a/package.json +++ b/package.json @@ -1868,6 +1868,7 @@ "dependencies": { "applicationinsights": "0.21.0", "copy-paste": "1.3.0", + "datauri": "^1.0.5", "iconv-lite": "0.4.19", "ignore": "3.3.5", "lodash.debounce": "4.0.8", diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 116cfc8..68a59ac 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -1,9 +1,10 @@ import { Dates, Strings } from '../system'; import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode'; -import { DiffWithCommand, ShowQuickCommitDetailsCommand } from '../commands'; +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; @@ -25,6 +26,25 @@ 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 = ` + + +`; + +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) { @@ -47,7 +67,7 @@ export class Annotations { return '#793738'; } - static getHoverMessage(commit: GitCommit, dateFormat: string | null): MarkdownString { + static getHoverMessage(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): MarkdownString { if (dateFormat === null) { dateFormat = 'MMMM Do, YYYY h:MMa'; } @@ -64,7 +84,11 @@ export class Annotations { message = `\n\n> ${message}`; } - const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})   __${commit.author}__, ${commit.fromNow()}   _(${commit.formatDate(dateFormat)})_${message}`); + const openInRemoteCommand = hasRemotes + ? `${' '.repeat(3)} [![](${getLinkIconDataUri(themeForegroundColor)})](${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; } @@ -74,8 +98,8 @@ export class Annotations { const codeDiff = this._getCodeDiff(chunkLine); const markdown = new MarkdownString(commit.isUncommitted - ? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)})   ${GlyphChars.Dash}   _uncommitted_\n${codeDiff}` - : `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)})   ${GlyphChars.Dash}   [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)}) ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})\n${codeDiff}`); + ? `[\`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; } @@ -97,8 +121,8 @@ export class Annotations { } as DecorationOptions; } - static detailsHover(commit: GitCommit, dateFormat: string | null): DecorationOptions { - const message = this.getHoverMessage(commit, dateFormat); + static detailsHover(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): DecorationOptions { + const message = this.getHoverMessage(commit, dateFormat, hasRemotes); return { hoverMessage: message } as DecorationOptions; @@ -163,9 +187,9 @@ export class Annotations { } as IRenderOptions; } - static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean, dateFormat: string | null): DecorationOptions { + static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean, dateFormat: string | null, hasRemotes: boolean): DecorationOptions { return { - hoverMessage: this.getHoverMessage(commit, dateFormat), + hoverMessage: this.getHoverMessage(commit, dateFormat, hasRemotes), renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined } as DecorationOptions; } diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index f177de4..f0b9e7a 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -39,18 +39,17 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { const separateLines = this._config.theme.annotations.file.gutter.separateLines; const decorations: DecorationOptions[] = []; + const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null); const document = this.document; let commit: GitBlameCommit | undefined; let compacted = false; let details: DecorationOptions | undefined; let gutter: DecorationOptions | undefined; + let hasRemotes: boolean | undefined; let previousSha: string | undefined; for (const l of blame.lines) { - commit = blame.commits.get(l.sha); - if (commit === undefined) continue; - const line = l.line + offset; if (previousSha === l.sha) { @@ -76,6 +75,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; gutter.range = new Range(line, 0, line, endIndex); + decorations.push(gutter); if (details !== undefined) { @@ -83,6 +83,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { details.range = cfg.hover.wholeLine ? document.validateRange(new Range(line, 0, line, endOfLineIndex)) : gutter.range; + decorations.push(details); } @@ -92,6 +93,31 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { compacted = false; previousSha = l.sha; + gutter = decorationsMap[l.sha]; + + if (gutter !== undefined) { + gutter = { ...gutter } as DecorationOptions; + + const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; + gutter.range = new Range(line, 0, line, endIndex); + + decorations.push(gutter); + + if (details !== undefined) { + details = { ...details } as DecorationOptions; + details.range = cfg.hover.wholeLine + ? document.validateRange(new Range(line, 0, line, endOfLineIndex)) + : gutter.range; + + decorations.push(details); + } + + continue; + } + + commit = blame.commits.get(l.sha); + if (commit === undefined) continue; + gutter = Annotations.gutter(commit, cfg.format, options, renderOptions); if (cfg.heatmap.enabled) { @@ -100,13 +126,20 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; gutter.range = new Range(line, 0, line, endIndex); + decorations.push(gutter); + decorationsMap[l.sha] = gutter; if (cfg.hover.details) { - details = Annotations.detailsHover(commit, dateFormat); + if (hasRemotes === undefined) { + hasRemotes = this.git.hasRemotes(commit.repoPath); + } + + details = Annotations.detailsHover(commit, dateFormat, hasRemotes); details.range = cfg.hover.wholeLine ? document.validateRange(new Range(line, 0, line, endOfLineIndex)) : gutter.range; + decorations.push(details); } } diff --git a/src/annotations/hoverBlameAnnotationProvider.ts b/src/annotations/hoverBlameAnnotationProvider.ts index e16c770..3befb97 100644 --- a/src/annotations/hoverBlameAnnotationProvider.ts +++ b/src/annotations/hoverBlameAnnotationProvider.ts @@ -24,18 +24,42 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { const dateFormat = this._config.defaultDateFormat; const decorations: DecorationOptions[] = []; + const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null); const document = this.document; let commit: GitBlameCommit | undefined; + let hasRemotes: boolean | undefined; let hover: DecorationOptions | undefined; for (const l of blame.lines) { + const line = l.line + offset; + + hover = decorationsMap[l.sha]; + + if (hover !== undefined) { + hover = { ...hover } as DecorationOptions; + + if (cfg.wholeLine) { + hover.range = document.validateRange(new Range(line, 0, line, endOfLineIndex)); + } + else { + const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex; + hover.range = new Range(line, 0, line, endIndex); + } + + decorations.push(hover); + + continue; + } + commit = blame.commits.get(l.sha); if (commit === undefined) continue; - const line = l.line + offset; + if (hasRemotes === undefined) { + hasRemotes = this.git.hasRemotes(commit.repoPath); + } - hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled, dateFormat); + hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled, dateFormat, hasRemotes); if (cfg.wholeLine) { hover.range = document.validateRange(new Range(line, 0, line, endOfLineIndex)); @@ -50,6 +74,8 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { } decorations.push(hover); + decorationsMap[l.sha] = hover; + } if (decorations.length) { diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index e6e1294..e73cd4c 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -46,7 +46,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { if (cfg.hover.details) { decorators.push({ - hoverMessage: Annotations.getHoverMessage(commit, dateFormat), + hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath)), range: range } as DecorationOptions); } diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index d52ad17..38f8d7d 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -12,6 +12,15 @@ export interface OpenCommitInRemoteCommandArgs { export class OpenCommitInRemoteCommand extends ActiveEditorCommand { + static getMarkdownCommandArgs(sha: string): string; + static getMarkdownCommandArgs(args: OpenCommitInRemoteCommandArgs): string; + static getMarkdownCommandArgs(argsOrSha: OpenCommitInRemoteCommandArgs | string): string { + const args = typeof argsOrSha === 'string' + ? { sha: argsOrSha } + : argsOrSha; + return super.getMarkdownCommandArgsCore(Commands.OpenCommitInRemote, args); + } + constructor(private git: GitService) { super(Commands.OpenCommitInRemote); } diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 94b3f63..9b5db7a 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -428,7 +428,7 @@ export class CurrentLineController extends Disposable { // I have no idea why I need this protection -- but it happens if (editor.document === undefined) return; - const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat); + const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes((logCommit || commit).repoPath)); decoration.range = editor.document.validateRange(new Range(line, showDetailsStartIndex, line, endOfLineIndex)); decorationOptions.push(decoration);