diff --git a/CHANGELOG.md b/CHANGELOG.md index 8499306..9417fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] -## [5.1.1-beta] +## [5.2.0-beta] ### Added -- Adds new `Changed Files` node to the `Repository Status` node of the `GitLens` custom view's `Repository View` (enabled via `"gitlens.insiders": true`) -- closes [#139](https://github.com/eamodio/vscode-gitlens/issues/139) - - Provides a file-based view of all the changed files in the working tree and/or files in commits that haven't yet been pushed upstream +- Adds new `Changed Files` node to the `Repository Status` node of the `GitLens` custom view's `Repository View` -- closes [#139](https://github.com/eamodio/vscode-gitlens/issues/139) + - Provides a file-based view of all the changed files in the working tree (enabled via `"gitlens.insiders": true`) and/or files in commits that haven't yet been pushed upstream - Adds `gitlens.gitExplorer.enabled` setting to specify whether or not to show the `GitLens` custom view - closes [#144](https://github.com/eamodio/vscode-gitlens/issues/144) ### Changed -- Chnages the default of the `gitlens.gitExplorer.commitFormat` setting to add parentheses around the commit id +- Changes the default of the `gitlens.gitExplorer.commitFormat` setting to add parentheses around the commit id - Removes many menu items from `editor/title` & `editor/title/context` by default -- can be re-enabled via the `gitlens.advanced.menus` setting ### Fixed +- Fixes [#146](https://github.com/eamodio/vscode-gitlens/issues/146) - Blame gutter annotation issue when commit contains emoji - Fixes an issue when running `Open File in Remote` with a multi-line selection wasn't properly opening the selection in GitLab -- thanks to [PR #145](https://github.com/eamodio/vscode-gitlens/pull/145) by Amanda Cameron ([@AmandaCameron](https://github.com/AmandaCameron))! - Fixes an issue where the `gitlens.advanced.menus` setting wasn't controlling all the menu items properly diff --git a/package-lock.json b/package-lock.json index 10464a5..b58fb46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,11 +47,6 @@ "json-stable-stringify": "1.0.1" } }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -1336,11 +1331,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", @@ -2334,15 +2324,6 @@ "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", "dev": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - } - }, "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", @@ -2358,14 +2339,6 @@ "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", "dev": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", diff --git a/package.json b/package.json index f37f17d..384916d 100644 --- a/package.json +++ b/package.json @@ -1921,7 +1921,6 @@ "lodash.once": "4.1.1", "moment": "2.18.1", "spawn-rx": "2.0.11", - "string-width": "2.1.1", "tmp": "0.0.33" }, "devDependencies": { diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 5869af9..4cdee36 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -1,4 +1,4 @@ -import { Dates, Strings } from '../system'; +import { Dates, Objects, Strings } from '../system'; import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode'; import { DiffWithCommand, OpenCommitInRemoteCommand, ShowQuickCommitDetailsCommand } from '../commands'; import { IThemeConfig, themeDefaults } from '../configuration'; @@ -133,9 +133,23 @@ export class Annotations { } as DecorationOptions; } - static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions { + static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig, options: ICommitFormatOptions): IRenderOptions { const cfgFileTheme = cfgTheme.annotations.file.gutter; + // Try to get the width of the string, if there is a cap + let width = 4; // Start with a padding + for (const token of Objects.values(options.tokenOptions)) { + if (token === undefined) continue; + + // If any token is uncapped, kick out and set no max + if (token.truncateTo == null) { + width = 0; + break; + } + + width += token.truncateTo; + } + let borderStyle = undefined; let borderWidth = undefined; if (heatmap.enabled) { @@ -152,7 +166,8 @@ export class Annotations { borderStyle: borderStyle, borderWidth: borderWidth, height: '100%', - margin: '0 26px -1px 0' + margin: '0 26px -1px 0', + width: (width > 4) ? `${width}ch` : undefined }, dark: { backgroundColor: cfgFileTheme.dark.backgroundColor || undefined, diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index 0625548..dfd2b4a 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -34,7 +34,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { const now = Date.now(); const offset = this.uri.offset; - const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap); + const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap, options); const separateLines = this._config.theme.annotations.file.gutter.separateLines; const decorations: DecorationOptions[] = []; @@ -58,7 +58,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { ...gutter.renderOptions, before: { ...gutter.renderOptions!.before, - contentText: GlyphChars.Space.repeat(Strings.getWidth(gutter.renderOptions!.before!.contentText!)) + contentText: GlyphChars.Space.repeat(Strings.width(gutter.renderOptions!.before!.contentText!)) } }; diff --git a/src/git/formatters/formatter.ts b/src/git/formatters/formatter.ts index 873891d..53a115b 100644 --- a/src/git/formatters/formatter.ts +++ b/src/git/formatters/formatter.ts @@ -51,7 +51,7 @@ export abstract class Formatter max) return truncate(s, max); return s; } export function padRight(s: string, padTo: number, padding: string = '\u00a0') { - const diff = padTo - getWidth(s); + const diff = padTo - width(s); return (diff <= 0) ? s : s + '\u00a0'.repeat(diff); } @@ -88,14 +83,14 @@ export namespace Strings { const left = max < 0; max = Math.abs(max); - const len = getWidth(s); + const len = width(s); if (len < max) return left ? padLeft(s, max, padding) : padRight(s, max, padding); if (len > max) return truncate(s, max); return s; } export function padRightOrTruncate(s: string, max: number, padding?: string) { - const len = getWidth(s); + const len = width(s); if (len < max) return padRight(s, max, padding); if (len > max) return truncate(s, max); return s; @@ -112,15 +107,15 @@ export namespace Strings { export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026') { if (!s) return s; - const len = getWidth(s); + const len = width(s); if (len <= truncateTo) return s; if (len === s.length) return `${s.substring(0, truncateTo - 1)}${ellipsis}`; // Skip ahead to start as far as we can by assuming all the double-width characters won't be truncated let chars = Math.floor(truncateTo / (len / s.length)); - let count = getWidth(s.substring(0, chars)); + let count = width(s.substring(0, chars)); while (count < truncateTo) { - count += getWidth(s[chars++]); + count += width(s[chars++]); } if (count >= truncateTo) { @@ -129,4 +124,107 @@ export namespace Strings { return `${s.substring(0, chars)}${ellipsis}`; } + + const ansiRegex = /[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))/g; + + export function width(s: string): number { + if (!s || s.length === 0) return 0; + + s = s.replace(ansiRegex, ''); + + let count = 0; + let emoji = 0; + let joiners = 0; + + const graphemes = [...s]; + for (let i = 0; i < graphemes.length; i++) { + const code = graphemes[i].codePointAt(0)!; + + // Ignore control characters + if (code <= 0x1F || (code >= 0x7F && code <= 0x9F)) continue; + + // Ignore combining characters + if (code >= 0x300 && code <= 0x36F) continue; + + // https://stackoverflow.com/questions/30757193/find-out-if-character-in-string-is-emoji + if ( + (code >= 0x1F600 && code <= 0x1F64F) || // Emoticons + (code >= 0x1F300 && code <= 0x1F5FF) || // Misc Symbols and Pictographs + (code >= 0x1F680 && code <= 0x1F6FF) || // Transport and Map + (code >= 0x2600 && code <= 0x26FF) || // Misc symbols + (code >= 0x2700 && code <= 0x27BF) || // Dingbats + (code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors + (code >= 0x1F900 && code <= 0x1F9FF) || // Supplemental Symbols and Pictographs + (code >= 65024 && code <= 65039) || // Variation selector + (code >= 8400 && code <= 8447) // Combining Diacritical Marks for Symbols + ) { + if (code >= 0x1F3FB && code <= 0x1F3FF) continue; // emoji modifier fitzpatrick type + + emoji++; + count += 2; + continue; + } + + // Ignore zero-width joiners '\u200d' + if (code === 8205) { + joiners++; + count -= 2; + continue; + } + + // Surrogates + if (code > 0xFFFF) { + i++; + } + + count += isFullwidthCodePoint(code) ? 2 : 1; + } + + const offset = emoji - joiners; + if (offset > 1) { + count += offset - 1; + } + return count; + } + + function isFullwidthCodePoint(cp: number) { + // code points are derived from: + // http://www.unix.org/Public/UNIDATA/EastAsianWidth.txt + if ( + cp >= 0x1100 && ( + cp <= 0x115f || // Hangul Jamo + cp === 0x2329 || // LEFT-POINTING ANGLE BRACKET + cp === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (0x2e80 <= cp && cp <= 0x3247 && cp !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (0x3250 <= cp && cp <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (0x4e00 <= cp && cp <= 0xa4c6) || + // Hangul Jamo Extended-A + (0xa960 <= cp && cp <= 0xa97c) || + // Hangul Syllables + (0xac00 <= cp && cp <= 0xd7a3) || + // CJK Compatibility Ideographs + (0xf900 <= cp && cp <= 0xfaff) || + // Vertical Forms + (0xfe10 <= cp && cp <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (0xfe30 <= cp && cp <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (0xff01 <= cp && cp <= 0xff60) || + (0xffe0 <= cp && cp <= 0xffe6) || + // Kana Supplement + (0x1b000 <= cp && cp <= 0x1b001) || + // Enclosed Ideographic Supplement + (0x1f200 <= cp && cp <= 0x1f251) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (0x20000 <= cp && cp <= 0x3fffd) + ) + ) { + return true; + } + + return false; + } } \ No newline at end of file