diff --git a/README.md b/README.md index f11b2ac..67009e2 100644 --- a/README.md +++ b/README.md @@ -5,56 +5,67 @@ # GitLens -GitLens **supercharges** the built-in Visual Studio Code Git capabilities. It helps you to **visualize code authorship** at a glance via inline Git blame annotations and code lens, **seamlessly navigate and explore** the history of a file or branch, **gain valuable insights** via powerful comparision commands, and so much more. +GitLens **supercharges** the built-in Visual Studio Code Git capabilities. It helps you to **visualize code authorship** at a glance via Git blame annotations and code lens, **seamlessly navigate and explore** the history of a file or branch, **gain valuable insights** via powerful comparision commands, and so much more. -GitLens provides an unobtrusive blame annotation at the end of the selected line, a status bar item showing the commit author and date of the selected line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the selected line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings). +GitLens provides an unobtrusive blame annotation at the end of the current line, a status bar item showing the commit author and date of the current line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the current line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings). ## Previews -#### Featuring code lens, whole file inline blame annotations, and navigation and exploration via quick pick menus +#### Featuring code lens, file blame annotations, and navigation and exploration via quick pick menus ![GitLens preview 1](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview1.gif) -#### Featuring selected line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more +#### Featuring current line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more ![GitLens preview 2](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview2.gif) ## Features #### Git Blame Annotations -- Adds a **blame annotation** to the end of the selected line showing the commit id and message, with more details (including the line's previous version) in a hover popup ([optional](#extension-settings), on by default) - -- Adds a `Toggle Blame Annotations` command (`gitlens.toggleBlame`) with a shortcut of `alt+b` to toggle **inline Git blame annotations** for a whole file with multiple styles — compact, expanded, and trailing - - Also adds a `Show Blame Annotations` command (`gitlens.showBlame`) - -- Adds **author and date blame information** about the selected line to the **status bar** ([optional](#extension-settings), on by default) - - By default clicking on the status bar shows a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more - - - Provides [customizable](#extension-settings) click behavior of the status bar — choose between one of the following - - Toggle whole file blame annotations on and off - - Toggle code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set - - Compare the file with the previous commit - - Show a quick pick menu with details and commands for the commit +- Adds a [customizable](#line-blame-annotation-settings) **Git blame annotation** to the end of the current line ([optional](#line-blame-annotation-settings), on by default) + - Contains the commit author, date, and message, by [default](#line-blame-annotation-settings) + - Commit details, including the changes from the line's previous version, are provided in a hover popup ([optional](#line-blame-annotation-settings), on by default) + +- Adds on-demand, highly [customizable](#file-blame-annotation-settings) **Git blame annotations** of the whole file + - Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings) + - Contains the commit message and date, by [default](#file-blame-annotation-settings) + - Commit details are also provided in a hover popup ([optional](#file-blame-annotation-settings), on by default) + +- Adds [customizable](#status-bar-settings) **blame information** about the current line to the **status bar** ([optional](#status-bar-settings), on by default) + - Contains the commit author and date, by [default](#status-bar-settings) + - Clicking the status bar item will, by [default](#status-bar-settings), show a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more + - Provides [customizable](#status-bar-settings) click behavior — choose between one of the following + - Toggle file blame annotations on and off + - Toggle code lens on and off + - Compare the line commit with the previous commit + - Compare the line commit with the working tree + - Show a quick pick menu with details and commands for the commit (default) - Show a quick pick menu with file details and commands for the commit - Show a quick pick menu with the commit history of the file - Show a quick pick menu with the commit history of the current branch +- Adds a `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the file blame annotations on and off + - Also adds a `Show File Blame Annotations` command (`gitlens.showFileBlame`) + +- Adds a `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) to toggle the current line blame annotations on and off + - Also adds a `Show Line Blame Annotations` command (`gitlens.showLineBlame`) + #### Git Code Lens -- Adds **code lens** to the top of the file and on code blocks ([optional](#extension-settings), on by default) +- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default) - **Recent Change** — author and date of the most recent commit for the file or code block - - By default, clicking on the code lens shows a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more + - Clicking the code lens will, by [default](#code-lens-settings), show a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more - **Authors** — number of authors of the file or code block and the most prominent author (if there is more than one) - - By default, clicking on the code lens toggles the inline Git blame annotations on and off for the whole file + - Clicking the code lens will, by [default](#code-lens-settings), toggle the file Git blame annotations on and off of the whole file - Will be hidden if the author of the most recent commit is also the only author of the file or block, to avoid duplicate information and reduce visual noise -- Provides [customizable](#extension-settings) click behavior for each code lens — choose between one of the following - - Toggle whole file blame annotations on and off - - Compare the file with the previous commit +- Provides [customizable](#code-lens-settings) click behavior for each code lens — choose between one of the following + - Toggle file blame annotations on and off + - Compare the commit with the previous commit - Show a quick pick menu with details and commands for the commit - Show a quick pick menu with file details and commands for the commit - Show a quick pick menu with the commit history of the file - Show a quick pick menu with the commit history of the current branch -- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set +- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off #### Powerful Comparison Tools @@ -180,40 +191,109 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line ## Insiders -Add [`"gitlens.insiders": true`](#extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues. +Add [`"gitlens.insiders": true`](#general-extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues. ## Extension Settings GitLens is highly customizable and provides many configuration settings to allow the personalization of almost all features +### General Settings + |Name | Description |-----|------------ |`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features |`gitlens.outputLevel`|Specifies how much (if any) output will be sent to the GitLens output channel -|`gitlens.blame.annotation.activeLine`|Specifies whether and how to show blame annotations on the active line. `off` - no annotation. `inline` - adds a trailing annotation to the active line. `hover` - adds hover annotation to the active line. `both` - adds both `inline` and `hover` annotations -|`gitlens.blame.annotation.activeLineDarkColor`|Specifies the color of the active line blame annotation to use with a dark theme. Must be a valid css color -|`gitlens.blame.annotation.activeLineLightColor`|Specifies the color of the active line blame annotation to use with a light theme. Must be a valid css color -|`gitlens.blame.annotation.highlight`|Specifies whether and how to highlight blame annotations. `none` - no highlight. `gutter` - adds a gutter icon. `line` - adds a full-line highlight. `both` - adds both `gutter` and `line` highlights -|`gitlens.blame.annotation.style`|Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation on every line -|`gitlens.blame.annotation.author`|Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles -|`gitlens.blame.annotation.date`|Specifies whether and how the commit date will be shown in the blame annotations. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.blame.annotation.dateFormat`. Applies only to the `expanded` & `trailing` annotation styles -|`gitlens.blame.annotation.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats -|`gitlens.blame.annotation.message`|Specifies whether the commit message will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles -|`gitlens.blame.annotation.sha`|Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles -|`gitlens.codeLens.visibility`|Specifies when code lens will be shown in the active document. `auto` - always shown. `ondemand` - never shown, unless toggled via the `gitlens.toggleCodeLens` command. `off` - never shown -|`gitlens.codeLens.authors.enabled`|Specifies whether the authors code lens is shown -|`gitlens.codeLens.authors.command`|Specifies the command executed when the authors code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick -|`gitlens.codeLens.recentChange.enabled`|Specifies whether the recent change code lens is shown -|`gitlens.codeLens.recentChange.command`|"Specifies the command executed when the recent change code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick -|`gitlens.codeLens.location`|Specifies where code lens will be rendered in the active document. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `gitlens.codeLens.locationCustomSymbols` -|`gitlens.codeLens.locationCustomSymbols`|Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind` -|`gitlens.codeLens.languageLocations`|Specifies where code lens will be rendered in the active document for the specified languages -|`gitlens.menus.diff.enabled`|Specifies whether diff commands will be added to the context menus -|`gitlens.statusBar.enabled`|Specifies whether blame information is shown in the status bar + +### Blame Annotation Settings + +#### File Blame Annotation Settings + +|Name | Description +|-----|------------ +|`gitlens.blame.file.annotationType`|Specifies the type of blame annotations that will be shown for the current file. `gutter` - adds an annotation to the beginning of each line. `hover` - shows annotations when hovering over each line +|`gitlens.blame.file.lineHighlight.enabled`|Specifies whether or not to highlight lines associated with the current line +|`gitlens.blame.file.lineHighlight.locations`|Specifies where the associated line highlights will be shown. `gutter` - adds a gutter glyph. `line` - adds a full-line highlight background color. `overviewRuler` - adds a decoration to the overviewRuler (scroll bar) +|`gitlens.annotations.file.gutter.format`|Specifies the format of the gutter blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.file.dateFormat`), `${authorAgo}` - commit author, relative commit date +|`gitlens.annotations.file.gutter.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats +|`gitlens.annotations.file.gutter.compact`|Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations +|`gitlens.annotations.file.gutter.heatmap.enabled`|Specifies whether or not to provide a heatmap indicator in the gutter blame annotations +|`gitlens.annotations.file.gutter.heatmap.location`|Specifies where the heatmap indicators will be shown in the gutter blame annotations. `left` - adds a heatmap indicator on the left edge of the gutter blame annotations. `right` - adds a heatmap indicator on the right edge of the gutter blame annotations +|`gitlens.annotations.file.gutter.hover.details`|Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations +|`gitlens.annotations.file.gutter.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line +|`gitlens.annotations.file.hover.heatmap.enabled`|Specifies whether or not to provide heatmap indicators on the left edge of each line +|`gitlens.annotations.file.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line + +#### Line Blame Annotation Settings + +|Name | Description +|-----|------------ +|`gitlens.blame.line.enabled`|Specifies whether or not to provide a blame annotation for the current line +|`gitlens.blame.line.annotationType`|Specifies the type of blame annotations that will be shown for the current line. `trailing` - adds an annotation to the end of the current line. `hover` - shows annotations when hovering over the current line +|`gitlens.annotations.line.trailing.format`|Specifies the format of the trailing blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.currentLine.dateFormat`), `${authorAgo}` - commit author, relative commit date +|`gitlens.annotations.line.trailing.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats +|`gitlens.annotations.line.trailing.hover.details`|Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations +|`gitlens.annotations.line.trailing.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations +|`gitlens.annotations.line.trailing.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line +|`gitlens.annotations.line.hover.details`|Specifies whether or not to provide a commit details hover annotation for the current line +|`gitlens.annotations.line.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation for the current line + +### Code Lens Settings + +|Name | Description +|-----|------------ +|`gitlens.codeLens.enabled`|Specifies whether or not to provide any Git code lens +|`gitlens.codeLens.recentChange.enabled`|Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block +|`gitlens.codeLens.recentChange.command`|Specifies the command to be executed when the `recent change` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick +|`gitlens.codeLens.authors.enabled`|Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one) +|`gitlens.codeLens.authors.command`|Specifies the command to be executed when the `authors` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick +|`gitlens.codeLens.locations`|Specifies where Git code lens will be shown in the document. `document` - adds code lens at the top of the document. `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc). `blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines. `custom` - adds code lens at the start of symbols contained in `gitlens.codeLens.locationCustomSymbols` +|`gitlens.codeLens.customLocationSymbols`|Specifies the set of document symbols where Git code lens will be shown in the document +|`gitlens.codeLens.perLanguageLocations`|Specifies where Git code lens will be shown in the document for the specified languages + +### Status Bar Settings + +|Name | Description +|-----|------------ +|`gitlens.statusBar.enabled`|Specifies whether or not to provide blame information on the status bar |`gitlens.statusBar.alignment`|Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right -|`gitlens.statusBar.command`|Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick -|`gitlens.statusBar.date`|Specifies whether and how the commit date will be shown in the blame status bar. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.statusBar.dateFormat` -|`gitlens.statusBar.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats +|`gitlens.statusBar.format`|Specifies the format of the blame information on the status bar. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`) +|`gitlens.statusBar.command`|Specifies the command to be executed when the blame status bar item is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick +|`gitlens.statusBar.format`|Specifies the format of the status bar blame information. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`), `${authorAgo}` - commit author, relative commit date +|`gitlens.statusBar.dateFormat`|Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats + +### Theme Settings + +|Name | Description +|-----|------------ +|`gitlens.theme.annotations.file.gutter.separateLines`|Specifies whether or not gutter blame annotations will be separated by a small gap +|`gitlens.theme.annotations.file.gutter.dark.backgroundColor`|Specifies the dark theme background color of the gutter blame annotations +|`gitlens.theme.annotations.file.gutter.light.backgroundColor`|Specifies the light theme background color of the gutter blame annotations +|`gitlens.theme.annotations.file.gutter.dark.foregroundColor`|Specifies the dark theme foreground color of the gutter blame annotations +|`gitlens.theme.annotations.file.gutter.light.foregroundColor`|Specifies the light theme foreground color of the gutter blame annotations +|`gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor`|Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations +|`gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor`|Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations +|`gitlens.theme.annotations.file.hover.separateLines`|Specifies whether or not hover blame annotations will be separated by a small gap (if heatmap is enabled) +|`gitlens.theme.annotations.line.trailing.dark.backgroundColor`|Specifies the dark theme background color of the trailing blame annotation +|`gitlens.theme.annotations.line.trailing.light.backgroundColor`|Specifies the light theme background color of the trailing blame annotation +|`gitlens.theme.annotations.line.trailing.dark.foregroundColor`|Specifies the dark theme foreground color of the trailing blame annotation +|`gitlens.theme.annotations.line.trailing.light.foregroundColor`|Specifies the light theme foreground color of the trailing blame annotation +|`gitlens.theme.lineHighlight.dark.backgroundColor`|Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color +|`gitlens.theme.lineHighlight.light.backgroundColor`|Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color +|`gitlens.theme.lineHighlight.dark.overviewRulerColor`|Specifies the dark theme overview ruler color of the associated line highlights in blame annotations +|`gitlens.theme.lineHighlight.light.overviewRulerColor`|Specifies the light theme overview ruler color of the associated line highlights in blame annotations + +### Advanced Settings + +|Name | Description +|-----|------------ +|`gitlens.advanced.toggleWhitespace.enabled`|Specifies whether or not to toggle whitespace off then showing blame annotations (*may* be required by certain fonts/themes) +|`gitlens.advanced.menus`|Specifies which commands will be added to the menus +|`gitlens.advanced.caching.enabled`|Specifies whether git output will be cached +|`gitlens.advanced.caching.maxLines`|Specifies the threshold for caching larger documents +|`gitlens.advanced.git`|Specifies the git path to use +|`gitlens.advanced.gitignore.enabled`|Specifies whether or not to parse the root .gitignore file for better performance (i.e. avoids blaming excluded files) +|`gitlens.advanced.maxQuickHistory`|Specifies the maximum number of QuickPick history entries to show +|`gitlens.advanced.quickPick.closeOnFocusOut`|Specifies whether or not to close the QuickPick menu when focus is lost ## Known Issues diff --git a/images/blame-dark.svg b/images/blame-dark.svg index bb26282..7e49611 100644 --- a/images/blame-dark.svg +++ b/images/blame-dark.svg @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/images/blame-light.svg b/images/blame-light.svg index 5fabefb..b60013b 100644 --- a/images/blame-light.svg +++ b/images/blame-light.svg @@ -1,6 +1,6 @@ - + diff --git a/package.json b/package.json index 2cea74d..271109b 100644 --- a/package.json +++ b/package.json @@ -1,929 +1,1263 @@ -{ - "name": "gitlens", - "version": "3.6.1", - "author": { - "name": "Eric Amodio", - "email": "eamodio@gmail.com" - }, - "publisher": "eamodio", - "engines": { - "vscode": "^1.12.0" - }, - "license": "SEE LICENSE IN LICENSE", - "displayName": "Git Lens \u2014 git blame annotations, code lens, and more", - "description": "Supercharge Visual Studio Code's Git capabilities \u2014 Visualize code authorship at a glance via inline Git blame annotations and code lens, seamlessly navigate and explore the history of a file or branch, gain valuable insights via powerful comparision commands, and so much more", - "badges": [ - { - "url": "https://badges.gitter.im/vscode-gitlens/Lobby.svg", - "href": "https://gitter.im/vscode-gitlens/Lobby", - "description": "Chat at https://gitter.im/vscode-gitlens/Lobby" - } - ], - "categories": [ - "Other" - ], - "keywords": [ - "git", - "code lens", - "blame", - "history", - "annotation", - "log", - "inline blame", - "compare", - "diff" - ], - "galleryBanner": { - "color": "#56098c", - "theme": "dark" - }, - "icon": "images/gitlens-icon.svg", - "preview": false, - "homepage": "https://github.com/eamodio/vscode-gitlens/blob/master/README.md", - "bugs": { - "url": "https://github.com/eamodio/vscode-gitlens/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/eamodio/vscode-gitlens.git" - }, - "main": "./out/src/extension", - "contributes": { - "configuration": { - "type": "object", - "title": "GitLens configuration", - "properties": { - "gitlens.debug": { - "type": "boolean", - "default": false, - "description": "Specifies debug mode" - }, - "gitlens.insiders": { - "type": "boolean", - "default": false, - "description": "Specifies whether or not to enable new experimental features (expect there to be issues)" - }, - "gitlens.outputLevel": { - "type": "string", - "default": "silent", - "enum": [ - "silent", - "errors", - "verbose" - ], - "description": "Specifies how much (if any) output will be sent to the GitLens output channel" - }, - "gitlens.blame.annotation.activeLine": { - "type": "string", - "default": "both", - "enum": [ - "off", - "inline", - "hover", - "both" - ], - "description": "Specifies whether and how to show blame annotations on the active line. `off` - no annotation. `inline` - adds a trailing annotation to the active line. `hover` - adds hover annotation to the active line. `both` - adds both `inline` and `hover` annotations" - }, - "gitlens.blame.annotation.activeLineDarkColor": { - "type": "string", - "default": "rgba(153, 153, 153, 0.35)", - "description": "Specifies the color of the active line blame annotation to use with a dark theme. Must be a valid css color" - }, - "gitlens.blame.annotation.activeLineLightColor": { - "type": "string", - "default": "rgba(153, 153, 153, 0.35)", - "description": "Specifies the color of the active line blame annotation to use with a light theme. Must be a valid css color" - }, - "gitlens.blame.annotation.highlight": { - "type": "string", - "default": "both", - "enum": [ - "none", - "gutter", - "line", - "both" - ], - "description": "Specifies whether and how to highlight blame annotations. `none` - no highlight. `gutter` - adds a gutter icon. `line` - adds a full-line highlight. `both` - adds both `gutter` and `line` highlights" - }, - "gitlens.blame.annotation.style": { - "type": "string", - "default": "expanded", - "enum": [ - "compact", - "expanded", - "trailing" - ], - "description": "Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation before every line. `trailing` - shows an annotation after every line" - }, - "gitlens.blame.annotation.author": { - "type": "boolean", - "default": true, - "description": "Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" - }, - "gitlens.blame.annotation.date": { - "type": "string", - "default": "off", - "enum": [ - "off", - "relative", - "absolute" - ], - "description": "Specifies whether and how the commit date will be shown in the blame annotations. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.blame.annotation.dateFormat`. Applies only to the `expanded` & `trailing` annotation styles" - }, - "gitlens.blame.annotation.dateFormat": { - "type": "string", - "default": null, - "description": "Specifies the date format of how absolute dates will be shown in the blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats" - }, - "gitlens.blame.annotation.message": { - "type": "boolean", - "default": false, - "description": "Specifies whether the commit message will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" - }, - "gitlens.blame.annotation.sha": { - "type": "boolean", - "default": true, - "description": "Specifies whether the commit id (sha) will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" - }, - "gitlens.codeLens.visibility": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "ondemand", - "off" - ], - "description": "Specifies when code lens will be shown in the active document. `auto` - always shown. `ondemand` - never shown, unless toggled via the `gitlens.toggleCodeLens` command. `off` - never shown" - }, - "gitlens.codeLens.authors.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether the authors code lens is shown" - }, - "gitlens.codeLens.authors.command": { - "type": "string", - "default": "gitlens.toggleBlame", - "enum": [ - "gitlens.toggleBlame", - "gitlens.showBlameHistory", - "gitlens.showFileHistory", - "gitlens.diffWithPrevious", - "gitlens.showQuickCommitDetails", - "gitlens.showQuickCommitFileDetails", - "gitlens.showQuickFileHistory", - "gitlens.showQuickRepoHistory" - ], - "description": "Specifies the command executed when the authors code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" - }, - "gitlens.codeLens.recentChange.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether the recent change code lens is shown" - }, - "gitlens.codeLens.recentChange.command": { - "type": "string", - "default": "gitlens.showQuickCommitFileDetails", - "enum": [ - "gitlens.toggleBlame", - "gitlens.showBlameHistory", - "gitlens.showFileHistory", - "gitlens.diffWithPrevious", - "gitlens.showQuickCommitDetails", - "gitlens.showQuickCommitFileDetails", - "gitlens.showQuickFileHistory", - "gitlens.showQuickRepoHistory" - ], - "description": "Specifies the command executed when the recent change code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" - }, - "gitlens.codeLens.location": { - "type": "string", - "default": "document+containers", - "enum": [ - "all", - "document+containers", - "document", - "custom" - ], - "description": "Specifies where code lens will be rendered in the active document. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `gitlens.codeLens.locationCustomSymbols`" - }, - "gitlens.codeLens.locationCustomSymbols": { - "type": "array", - "description": "Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind`" - }, - "gitlens.codeLens.languageLocations": { - "type": "array", - "default": [ - { - "language": "css", - "location": "document" - }, - { - "language": "html", - "location": "document" - }, - { - "language": "json", - "location": "document" - }, - { - "language": "less", - "location": "document" - }, - { - "language": "scss", - "location": "document" - }, - { - "language": "vue", - "location": "document" - } - ], - "items": { - "type": "object", - "required": [ - "language", - "location" - ], - "properties": { - "language": { - "type": "string", - "description": "Specifies the language to which this code lens override applies" - }, - "location": { - "type": "string", - "default": "document+containers", - "enum": [ - "all", - "document+containers", - "document", - "custom", - "none" - ], - "description": "Specifies where code lens will be rendered in the active document for the specified language. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `customSymbols`" - }, - "customSymbols": { - "type": "string", - "description": "Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind`" - } - } - }, - "uniqueItems": true, - "enum": [ - "all", - "document+containers", - "document", - "custom" - ], - "description": "Specifies where code lens will be rendered in the active document for the specified languages" - }, - "gitlens.codeLens.debug": { - "type": "boolean", - "default": false, - "description": "Specifies whether or not to show debug information in code lens" - }, - "gitlens.menus.diff.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether diff commands will be added to the context menus" - }, - "gitlens.statusBar.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether blame information is shown in the status bar" - }, - "gitlens.statusBar.alignment": { - "type": "string", - "default": "right", - "enum": [ - "left", - "right" - ], - "description": "Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right" - }, - "gitlens.statusBar.command": { - "type": "string", - "default": "gitlens.showQuickCommitDetails", - "enum": [ - "gitlens.toggleBlame", - "gitlens.showBlameHistory", - "gitlens.showFileHistory", - "gitlens.diffWithPrevious", - "gitlens.diffWithWorking", - "gitlens.toggleCodeLens", - "gitlens.showQuickCommitDetails", - "gitlens.showQuickCommitFileDetails", - "gitlens.showQuickFileHistory", - "gitlens.showQuickRepoHistory" - ], - "description": "Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" - }, - "gitlens.statusBar.date": { - "type": "string", - "default": "relative", - "enum": [ - "off", - "relative", - "absolute" - ], - "description": "Specifies whether and how the commit date will be shown in the blame status bar. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.statusBar.dateFormat`" - }, - "gitlens.statusBar.dateFormat": { - "type": "string", - "default": null, - "description": "Specifies the date format of how absolute dates will be shown in the blame status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats" - }, - "gitlens.advanced.caching.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether git blame output will be cached" - }, - "gitlens.advanced.caching.statusBar.maxLines": { - "type": "number", - "default": 0, - "description": "Specifies whether status bar git blame output will be cached for larger documents" - }, - "gitlens.advanced.git": { - "type": "string", - "default": null, - "description": "Specifies a git path to use" - }, - "gitlens.advanced.gitignore.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether or not to parse the root .gitignore file for better performance (i.e. avoids blaming excluded files)" - }, - "gitlens.advanced.maxQuickHistory": { - "type": "number", - "default": 200, - "description": "Specifies the maximum number of QuickPick history entries to show" - }, - "gitlens.advanced.quickPick.closeOnFocusOut": { - "type": "boolean", - "default": true, - "description": "Specifies whether or not to close the QuickPick menu when focus is lost" - }, - "gitlens.advanced.toggleWhitespace.enabled": { - "type": "boolean", - "default": false, - "description": "Specifies whether or not to toggle whitespace off then showing blame annotations (*may* be required by certain fonts/themes)" - } - } - }, - "commands": [ - { - "command": "gitlens.diffDirectory", - "title": "Directory Compare", - "category": "GitLens" - }, - { - "command": "gitlens.diffWithBranch", - "title": "Compare File with...", - "category": "GitLens" - }, - { - "command": "gitlens.diffWithNext", - "title": "Compare File with Next Commit", - "category": "GitLens" - }, - { - "command": "gitlens.diffWithPrevious", - "title": "Compare File with Previous", - "category": "GitLens" - }, - { - "command": "gitlens.diffLineWithPrevious", - "title": "Compare Line Commit with Previous", - "category": "GitLens" - }, - { - "command": "gitlens.diffWithWorking", - "title": "Compare File with Working Tree", - "category": "GitLens" - }, - { - "command": "gitlens.diffLineWithWorking", - "title": "Compare Line Commit with Working Tree", - "category": "GitLens" - }, - { - "command": "gitlens.showBlame", - "title": "Show Blame Annotations", - "category": "GitLens" - }, - { - "command": "gitlens.toggleBlame", - "title": "Toggle Blame Annotations", - "category": "GitLens", - "icon": { - "dark": "images/git-icon-dark.svg", - "light": "images/git-icon-light.svg" - } - }, - { - "command": "gitlens.toggleCodeLens", - "title": "Toggle Git Code Lens", - "category": "GitLens" - }, - { - "command": "gitlens.showBlameHistory", - "title": "Open Blame History Explorer", - "category": "GitLens" - }, - { - "command": "gitlens.showCommitSearch", - "title": "Search Commits", - "category": "GitLens" - }, - { - "command": "gitlens.showFileHistory", - "title": "Open File History Explorer", - "category": "GitLens" - }, - { - "command": "gitlens.showLastQuickPick", - "title": "Show Last Opened Quick Pick", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickCommitDetails", - "title": "Show Commit Details", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickCommitFileDetails", - "title": "Show Line Commit Details", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickFileHistory", - "title": "Show File History", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickBranchHistory", - "title": "Show Branch History", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickRepoHistory", - "title": "Show Current Branch History", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickRepoStatus", - "title": "Show Repository Status", - "category": "GitLens" - }, - { - "command": "gitlens.showQuickStashList", - "title": "Show Stashed Changes", - "category": "GitLens" - }, - { - "command": "gitlens.copyShaToClipboard", - "title": "Copy Commit ID to Clipboard", - "category": "GitLens" - }, - { - "command": "gitlens.copyMessageToClipboard", - "title": "Copy Commit Message to Clipboard", - "category": "GitLens" - }, - { - "command": "gitlens.closeUnchangedFiles", - "title": "Close Unchanged Files", - "category": "GitLens" - }, - { - "command": "gitlens.openChangedFiles", - "title": "Open Changed Files", - "category": "GitLens" - }, - { - "command": "gitlens.openBranchInRemote", - "title": "Open Branch in Remote", - "category": "GitLens" - }, - { - "command": "gitlens.openCommitInRemote", - "title": "Open Line Commit in Remote", - "category": "GitLens" - }, - { - "command": "gitlens.openFileInRemote", - "title": "Open File in Remote", - "category": "GitLens" - }, - { - "command": "gitlens.openRepoInRemote", - "title": "Open Repository in Remote", - "category": "GitLens" - }, - { - "command": "gitlens.stashApply", - "title": "Apply Stashed Changes", - "category": "GitLens" - }, - { - "command": "gitlens.stashSave", - "title": "Stash Changes", - "category": "GitLens" - } - ], - "menus": { - "commandPalette": [ - { - "command": "gitlens.diffDirectory", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.diffWithBranch", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.diffWithNext", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.diffWithPrevious", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.diffLineWithPrevious", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.diffWithWorking", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.diffLineWithWorking", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.showBlame", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.toggleBlame", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.toggleCodeLens", - "when": "gitlens:isTracked && gitlens:canToggleCodeLens" - }, - { - "command": "gitlens.showBlameHistory", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.showFileHistory", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.showLastQuickPick", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickCommitDetails", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.showQuickCommitFileDetails", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.showQuickFileHistory", - "when": "gitlens:isTracked" - }, - { - "command": "gitlens.showQuickBranchHistory", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickRepoHistory", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickRepoStatus", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickStashList", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.copyShaToClipboard", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.copyMessageToClipboard", - "when": "gitlens:isBlameable" - }, - { - "command": "gitlens.closeUnchangedFiles", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.openChangedFiles", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.openBranchInRemote", - "when": "gitlens:hasRemotes" - }, - { - "command": "gitlens.openCommitInRemote", - "when": "gitlens:isBlameable && gitlens:hasRemotes" - }, - { - "command": "gitlens.openFileInRemote", - "when": "gitlens:isTracked && gitlens:hasRemotes" - }, - { - "command": "gitlens.openRepoInRemote", - "when": "gitlens:hasRemotes" - }, - { - "command": "gitlens.stashApply", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.stashSave", - "when": "gitlens:enabled" - } - ], - "explorer/context": [ - { - "command": "gitlens.diffWithPrevious", - "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@1" - }, - { - "command": "gitlens.diffWithWorking", - "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@2" - }, - { - "command": "gitlens.showQuickFileHistory", - "when": "gitlens:enabled", - "group": "1_gitlens_1@1" - }, - { - "command": "gitlens.openFileInRemote", - "when": "gitlens:enabled", - "group": "1_gitlens_1@2" - } - ], - "editor/title": [ - { - "command": "gitlens.toggleBlame", - "when": "gitlens:isBlameable", - "group": "navigation@100" - }, - { - "command": "gitlens.diffWithPrevious", - "when": "editorTextFocus && gitlens:isTracked && config.gitlens.menus.diff.enabled", - "group": "2_gitlens" - }, - { - "command": "gitlens.diffWithWorking", - "when": "editorTextFocus && gitlens:isTracked && config.gitlens.menus.diff.enabled", - "group": "2_gitlens" - }, - { - "command": "gitlens.showQuickFileHistory", - "when": "editorFocus && gitlens:isTracked", - "group": "2_gitlens_1" - }, - { - "command": "gitlens.showQuickRepoHistory", - "when": "!editorFocus && gitlens:enabled", - "group": "2_gitlens_1" - }, - { - "command": "gitlens.showQuickRepoStatus", - "when": "gitlens:enabled", - "group": "2_gitlens_1" - } - ], - "editor/title/context": [ - { - "command": "gitlens.diffWithPrevious", - "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@1" - }, - { - "command": "gitlens.diffWithWorking", - "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@2" - }, - { - "command": "gitlens.showQuickFileHistory", - "when": "gitlens:enabled", - "group": "1_gitlens_1@1" - }, - { - "command": "gitlens.toggleBlame", - "when": "gitlens:enabled", - "group": "1_gitlens_1@2" - }, - { - "command": "gitlens.openFileInRemote", - "when": "gitlens:enabled", - "group": "1_gitlens_1@3" - } - ], - "editor/context": [ - { - "command": "gitlens.diffLineWithPrevious", - "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@1" - }, - { - "command": "gitlens.diffLineWithWorking", - "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.menus.diff.enabled", - "group": "1_gitlens@2" - }, - { - "command": "gitlens.showQuickCommitFileDetails", - "when": "editorTextFocus && gitlens:isBlameable", - "group": "1_gitlens@3" - }, - { - "command": "gitlens.diffWithPrevious", - "when": "editorTextFocus && gitlens:isTracked && config.gitlens.menus.diff.enabled", - "group": "1_gitlens_1@1" - }, - { - "command": "gitlens.diffWithWorking", - "when": "editorTextFocus && gitlens:isTracked && config.gitlens.menus.diff.enabled", - "group": "1_gitlens_1@2" - }, - { - "command": "gitlens.showQuickFileHistory", - "when": "gitlens:isTracked", - "group": "3_gitlens@1" - }, - { - "command": "gitlens.toggleBlame", - "when": "editorTextFocus && gitlens:isBlameable", - "group": "3_gitlens@2" - }, - { - "command": "gitlens.openFileInRemote", - "when": "editorTextFocus && gitlens:isTracked && gitlens:hasRemotes", - "group": "3_gitlens@3" - }, - { - "command": "gitlens.copyShaToClipboard", - "when": "editorTextFocus && gitlens:isBlameable", - "group": "9_gitlens@1" - }, - { - "command": "gitlens.copyMessageToClipboard", - "when": "editorTextFocus && gitlens:isBlameable", - "group": "9_gitlens@2" - } - ] - }, - "keybindings": [ - { - "command": "gitlens.key.left", - "key": "alt+left", - "when": "gitlens:key:left" - }, - { - "command": "gitlens.key.right", - "key": "alt+right", - "when": "gitlens:key:right" - }, - { - "command": "gitlens.key.,", - "key": "alt+,", - "when": "gitlens:key:," - }, - { - "command": "gitlens.key..", - "key": "alt+.", - "when": "gitlens:key:." - }, - { - "command": "gitlens.toggleBlame", - "key": "alt+b", - "mac": "alt+b", - "when": "editorTextFocus && gitlens:isTracked" - }, - { - "command": "gitlens.toggleCodeLens", - "key": "shift+alt+b", - "mac": "shift+alt+b", - "when": "editorTextFocus && gitlens:isTracked && gitlens:canToggleCodeLens" - }, - { - "command": "gitlens.showLastQuickPick", - "key": "alt+-", - "mac": "alt+-", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showCommitSearch", - "key": "alt+/", - "mac": "alt+/", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickFileHistory", - "key": "alt+h", - "mac": "alt+h", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickRepoHistory", - "key": "shift+alt+h", - "mac": "shift+alt+h", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickRepoStatus", - "key": "alt+s", - "mac": "alt+s", - "when": "gitlens:enabled" - }, - { - "command": "gitlens.showQuickCommitFileDetails", - "key": "alt+c", - "mac": "alt+c", - "when": "editorTextFocus && gitlens:enabled" - }, - { - "command": "gitlens.diffWithNext", - "key": "alt+.", - "mac": "alt+.", - "when": "editorTextFocus && gitlens:isTracked" - }, - { - "command": "gitlens.diffLineWithPrevious", - "key": "shift+alt+,", - "mac": "shift+alt+,", - "when": "editorTextFocus && gitlens:isTracked" - }, - { - "command": "gitlens.diffWithPrevious", - "key": "alt+,", - "mac": "alt+,", - "when": "editorTextFocus && gitlens:isTracked" - }, - { - "command": "gitlens.diffLineWithWorking", - "key": "alt+w", - "mac": "alt+w", - "when": "editorTextFocus && gitlens:isTracked" - }, - { - "command": "gitlens.diffWithWorking", - "key": "shift+alt+w", - "mac": "shift+alt+w", - "when": "editorTextFocus && gitlens:isTracked" - } - ] - }, - "activationEvents": [ - "*" - ], - "scripts": { - "clean": "git clean -xdf", - "compile": "tslint --project tslint.json && tsc -p ./", - "watch": "tsc -watch -p ./", - "lint": "tslint --project tslint.json", - "pack": "git clean -xdf && vsce package", - "postinstall": "node ./node_modules/vscode/bin/install", - "pub": "git clean -xdf && vsce publish", - "reset": "git clean -xdf && npm install", - "vscode:prepublish": "npm install --no-save && npm run compile" - }, - "dependencies": { - "applicationinsights": "0.20.1", - "copy-paste": "1.3.0", - "iconv-lite": "0.4.17", - "ignore": "3.3.3", - "lodash.debounce": "4.0.8", - "lodash.escaperegexp": "4.1.2", - "lodash.isequal": "4.5.0", - "lodash.once": "4.1.1", - "moment": "2.18.1", - "spawn-rx": "2.0.11", - "tmp": "0.0.31" - }, - "devDependencies": { - "@types/copy-paste": "1.1.30", - "@types/iconv-lite": "0.0.1", - "@types/mocha": "2.2.41", - "@types/node": "7.0.28", - "@types/tmp": "0.0.33", - "mocha": "3.4.2", - "tslint": "5.4.3", - "typescript": "2.3.4", - "vscode": "1.1.0" - } -} +{ + "name": "gitlens", + "version": "3.6.1", + "author": { + "name": "Eric Amodio", + "email": "eamodio@gmail.com" + }, + "publisher": "eamodio", + "engines": { + "vscode": "^1.12.0" + }, + "license": "SEE LICENSE IN LICENSE", + "displayName": "Git Lens \u2014 git blame annotations, code lens, and more", + "description": "Supercharge Visual Studio Code's Git capabilities \u2014 Visualize code authorship at a glance via Git blame annotations and code lens, seamlessly navigate and explore the history of a file or branch, gain valuable insights via powerful comparision commands, and so much more", + "badges": [ + { + "url": "https://badges.gitter.im/vscode-gitlens/Lobby.svg", + "href": "https://gitter.im/vscode-gitlens/Lobby", + "description": "Chat at https://gitter.im/vscode-gitlens/Lobby" + } + ], + "categories": [ + "Other" + ], + "keywords": [ + "git", + "code lens", + "blame", + "history", + "annotation", + "log", + "inline blame", + "compare", + "diff" + ], + "galleryBanner": { + "color": "#56098c", + "theme": "dark" + }, + "icon": "images/gitlens-icon.svg", + "preview": false, + "homepage": "https://github.com/eamodio/vscode-gitlens/blob/master/README.md", + "bugs": { + "url": "https://github.com/eamodio/vscode-gitlens/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/eamodio/vscode-gitlens.git" + }, + "main": "./out/src/extension", + "contributes": { + "configuration": { + "type": "object", + "title": "GitLens configuration", + "properties": { + "gitlens.debug": { + "type": "boolean", + "default": false, + "description": "Specifies debug mode" + }, + "gitlens.insiders": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not to enable new experimental features (expect there to be issues)" + }, + "gitlens.outputLevel": { + "type": "string", + "default": "silent", + "enum": [ + "silent", + "errors", + "verbose" + ], + "description": "Specifies how much (if any) output will be sent to the GitLens output channel" + }, + "gitlens.annotations.file.gutter.format": { + "type": "string", + "default": "${message|40?} ${ago|14-}", + "description": "Specifies the format of the gutter blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.file.dateFormat`), `${authorAgo}` - commit author, relative commit date" + }, + "gitlens.annotations.file.gutter.dateFormat": { + "type": "string", + "default": null, + "description": "Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats" + }, + "gitlens.annotations.file.gutter.compact": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations" + }, + "gitlens.annotations.file.gutter.heatmap.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a heatmap indicator in the gutter blame annotations" + }, + "gitlens.annotations.file.gutter.heatmap.location": { + "type": "string", + "default": "right", + "enum": [ + "left", + "right" + ], + "description": "Specifies where the heatmap indicators will be shown in the gutter blame annotations. `left` - adds a heatmap indicator on the left edge of the gutter blame annotations. `right` - adds a heatmap indicator on the right edge of the gutter blame annotations" + }, + "gitlens.annotations.file.gutter.hover.details": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations" + }, + "gitlens.annotations.file.gutter.hover.wholeLine": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not to trigger hover annotations over the whole line" + }, + "gitlens.annotations.file.hover.heatmap.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide heatmap indicators on the left edge of each line" + }, + "gitlens.annotations.file.hover.wholeLine": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to trigger hover annotations over the whole line" + }, + "gitlens.annotations.line.hover.details": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a commit details hover annotation for the current line" + }, + "gitlens.annotations.line.hover.changes": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a changes (diff) hover annotation for the current line" + }, + "gitlens.annotations.line.trailing.format": { + "type": "string", + "default": "${authorAgo} \u2022 ${message}", + "description": "Specifies the format of the trailing blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.currentLine.dateFormat`), `${authorAgo}` - commit author, relative commit date" + }, + "gitlens.annotations.line.trailing.dateFormat": { + "type": "string", + "default": null, + "description": "Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats" + }, + "gitlens.annotations.line.trailing.hover.details": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations" + }, + "gitlens.annotations.line.trailing.hover.changes": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations" + }, + "gitlens.annotations.line.trailing.hover.wholeLine": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not to trigger hover annotations over the whole line" + }, + "gitlens.blame.file.annotationType": { + "type": "string", + "default": "gutter", + "enum": [ + "gutter", + "hover" + ], + "description": "Specifies the type of blame annotations that will be shown for the current file. `gutter` - adds an annotation to the beginning of each line. `hover` - shows annotations when hovering over each line" + }, + "gitlens.blame.file.lineHighlight.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to highlight lines associated with the current line" + }, + "gitlens.blame.file.lineHighlight.locations": { + "type": "array", + "default": [ + "gutter", + "line", + "overviewRuler" + ], + "items": { + "type": "string", + "enum": [ + "gutter", + "line", + "overviewRuler" + ] + }, + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "description": "Specifies where the associated line highlights will be shown. `gutter` - adds a gutter glyph. `line` - adds a full-line highlight background color. `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)" + }, + "gitlens.blame.line.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a blame annotation for the current line" + }, + "gitlens.blame.line.annotationType": { + "type": "string", + "default": "trailing", + "enum": [ + "trailing", + "hover" + ], + "description": "Specifies the type of blame annotations that will be shown for the current line. `trailing` - adds an annotation to the end of the current line. `hover` - shows annotations when hovering over the current line" + }, + "gitlens.codeLens.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide any Git code lens" + }, + "gitlens.codeLens.recentChange.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block" + }, + "gitlens.codeLens.recentChange.command": { + "type": "string", + "default": "gitlens.showQuickCommitFileDetails", + "enum": [ + "gitlens.toggleFileBlame", + "gitlens.showBlameHistory", + "gitlens.showFileHistory", + "gitlens.diffWithPrevious", + "gitlens.showQuickCommitDetails", + "gitlens.showQuickCommitFileDetails", + "gitlens.showQuickFileHistory", + "gitlens.showQuickRepoHistory" + ], + "description": "Specifies the command to be executed when the `recent change` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + }, + "gitlens.codeLens.authors.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)" + }, + "gitlens.codeLens.authors.command": { + "type": "string", + "default": "gitlens.toggleFileBlame", + "enum": [ + "gitlens.toggleFileBlame", + "gitlens.showBlameHistory", + "gitlens.showFileHistory", + "gitlens.diffWithPrevious", + "gitlens.showQuickCommitDetails", + "gitlens.showQuickCommitFileDetails", + "gitlens.showQuickFileHistory", + "gitlens.showQuickRepoHistory" + ], + "description": "Specifies the command to be executed when the `authors` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + }, + "gitlens.codeLens.locations": { + "type": "array", + "default": [ + "document", + "containers" + ], + "items": { + "type": "string", + "enum": [ + "document", + "containers", + "blocks", + "custom" + ] + }, + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "description": "Specifies where Git code lens will be shown in the document. `document` - adds code lens at the top of the document. `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc). `blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines. `custom` - adds code lens at the start of symbols contained in `gitlens.codeLens.locationCustomSymbols`" + }, + "gitlens.codeLens.customLocationSymbols": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Specifies the set of document symbols where Git code lens will be shown in the document. Must be a member of `SymbolKind`" + }, + "gitlens.codeLens.perLanguageLocations": { + "type": "array", + "default": [ + { + "language": "css", + "locations": [ + "document" + ] + }, + { + "language": "html", + "locations": [ + "document" + ] + }, + { + "language": "json", + "locations": [ + "document" + ] + }, + { + "language": "less", + "locations": [ + "document" + ] + }, + { + "language": "scss", + "locations": [ + "document" + ] + }, + { + "language": "vue", + "locations": [ + "document" + ] + } + ], + "items": { + "type": "object", + "required": [ + "language", + "locations" + ], + "properties": { + "language": { + "type": "string", + "description": "Specifies the language to which this code lens override applies" + }, + "locations": { + "type": "array", + "default": [ + "document", + "containers" + ], + "items": { + "type": "string", + "enum": [ + "document", + "containers", + "blocks", + "custom" + ] + }, + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "description": "Specifies where Git code lens will be shown in the document for the specified language. `document` - adds code lens at the top of the document. `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc). `blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines. `custom` - adds code lens at the start of symbols contained in `customSymbols`" + }, + "customSymbols": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Specifies the set of document symbols where Git code lens will be shown in the document for the specified language. Must be a member of `SymbolKind`" + } + } + }, + "uniqueItems": true, + "description": "Specifies where Git code lens will be shown in the document for the specified languages" + }, + "gitlens.codeLens.debug": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not to show debug information in code lens" + }, + "gitlens.statusBar.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide blame information on the status bar" + }, + "gitlens.statusBar.alignment": { + "type": "string", + "default": "right", + "enum": [ + "left", + "right" + ], + "description": "Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right" + }, + "gitlens.statusBar.command": { + "type": "string", + "default": "gitlens.showQuickCommitDetails", + "enum": [ + "gitlens.toggleFileBlame", + "gitlens.showBlameHistory", + "gitlens.showFileHistory", + "gitlens.diffWithPrevious", + "gitlens.diffWithWorking", + "gitlens.toggleCodeLens", + "gitlens.showQuickCommitDetails", + "gitlens.showQuickCommitFileDetails", + "gitlens.showQuickFileHistory", + "gitlens.showQuickRepoHistory" + ], + "description": "Specifies the command to be executed when the blame status bar item is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick" + }, + "gitlens.statusBar.format": { + "type": "string", + "default": "${authorAgo}", + "description": "Specifies the format of the status bar blame information. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`), `${authorAgo}` - commit author, relative commit date" + }, + "gitlens.statusBar.dateFormat": { + "type": "string", + "default": null, + "description": "Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats" + }, + "gitlens.theme.annotations.file.gutter.separateLines": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not gutter blame annotations will be separated by a small gap" + }, + "gitlens.theme.annotations.file.gutter.dark.backgroundColor": { + "type": "string", + "default": "rgba(255, 255, 255, 0.075)", + "description": "Specifies the dark theme background color of the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.gutter.light.backgroundColor": { + "type": "string", + "default": "rgba(0, 0, 0, 0.05)", + "description": "Specifies the light theme background color of the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.gutter.dark.foregroundColor": { + "type": "string", + "default": "rgb(190, 190, 190)", + "description": "Specifies the dark theme foreground color of the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.gutter.light.foregroundColor": { + "type": "string", + "default": "rgb(116, 116, 116)", + "description": "Specifies the light theme foreground color of the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.6)", + "description": "Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.6)", + "description": "Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations. Must be a valid css color" + }, + "gitlens.theme.annotations.file.hover.separateLines": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not hover blame annotations will be separated by a small gap (if heatmap is enabled)" + }, + "gitlens.theme.annotations.line.trailing.dark.backgroundColor": { + "type": "string", + "default": null, + "description": "Specifies the dark theme background color of the trailing blame annotation. Must be a valid css color" + }, + "gitlens.theme.annotations.line.trailing.light.backgroundColor": { + "type": "string", + "default": null, + "description": "Specifies the light theme background color of the trailing blame annotation. Must be a valid css color" + }, + "gitlens.theme.annotations.line.trailing.dark.foregroundColor": { + "type": "string", + "default": "rgba(153, 153, 153, 0.35)", + "description": "Specifies the dark theme foreground color of the trailing blame annotation. Must be a valid css color" + }, + "gitlens.theme.annotations.line.trailing.light.foregroundColor": { + "type": "string", + "default": "rgba(153, 153, 153, 0.35)", + "description": "Specifies the light theme foreground color of the trailing blame annotation. Must be a valid css color" + }, + "gitlens.theme.lineHighlight.dark.backgroundColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.2)", + "description": "Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color" + }, + "gitlens.theme.lineHighlight.light.backgroundColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.2)", + "description": "Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color" + }, + "gitlens.theme.lineHighlight.dark.overviewRulerColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.6)", + "description": "Specifies the dark theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color" + }, + "gitlens.theme.lineHighlight.light.overviewRulerColor": { + "type": "string", + "default": "rgba(0, 188, 242, 0.6)", + "description": "Specifies the light theme overview ruler color of the associated line highlights in blame annotations. Must be a valid css color" + }, + "gitlens.advanced.caching.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether git output will be cached" + }, + "gitlens.advanced.caching.maxLines": { + "type": "number", + "default": 0, + "description": "Specifies the threshold for caching larger documents" + }, + "gitlens.advanced.git": { + "type": "string", + "default": null, + "description": "Specifies the git path to use" + }, + "gitlens.advanced.gitignore.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to parse the root .gitignore file for better performance (i.e. avoids blaming excluded files)" + }, + "gitlens.advanced.maxQuickHistory": { + "type": "number", + "default": 200, + "description": "Specifies the maximum number of QuickPick history entries to show" + }, + "gitlens.advanced.menus": { + "type": "object", + "default": { + "explorerContext": { + "fileDiff": true, + "history": true, + "remote": true + }, + "editorContext": { + "blame": true, + "copy": true, + "fileDiff": true, + "history": true, + "lineDiff": true, + "remote": true + }, + "editorTitle": { + "blame": true, + "fileDiff": true, + "history": true, + "status": true + }, + "editorTitleContext": { + "blame": true, + "fileDiff": true, + "history": true, + "remote": true + } + }, + "description": "Specifies which commands will be added to which menus", + "properties": { + "explorerContext": { + "type": "object", + "default": { + "fileDiff": true, + "history": true, + "remote": true + }, + "properties": { + "fileDiff": { + "type": "boolean", + "default": true + }, + "history": { + "type": "boolean", + "default": true + }, + "remote": { + "type": "boolean", + "default": true + } + } + }, + "editorContext": { + "type": "object", + "default": { + "blame": true, + "copy": true, + "fileDiff": true, + "history": true, + "lineDiff": true, + "remote": true + }, + "properties": { + "blame": { + "type": "boolean", + "default": true + }, + "copy": { + "type": "boolean", + "default": true + }, + "details": { + "type": "boolean", + "default": true + }, + "fileDiff": { + "type": "boolean", + "default": true + }, + "history": { + "type": "boolean", + "default": true + }, + "lineDiff": { + "type": "boolean", + "default": true + }, + "remote": { + "type": "boolean", + "default": true + } + } + }, + "editorTitle": { + "type": "object", + "default": { + "blame": true, + "fileDiff": true, + "history": true, + "status": true + }, + "properties": { + "blame": { + "type": "boolean", + "default": true + }, + "fileDiff": { + "type": "boolean", + "default": true + }, + "history": { + "type": "boolean", + "default": true + }, + "status": { + "type": "boolean", + "default": true + } + } + }, + "editorTitleContext": { + "type": "object", + "default": { + "blame": true, + "fileDiff": true, + "history": true, + "remote": true + }, + "properties": { + "blame": { + "type": "boolean", + "default": true + }, + "fileDiff": { + "type": "boolean", + "default": true + }, + "history": { + "type": "boolean", + "default": true + }, + "remote": { + "type": "boolean", + "default": true + } + } + } + } + }, + "gitlens.advanced.quickPick.closeOnFocusOut": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to close the QuickPick menu when focus is lost" + }, + "gitlens.advanced.toggleWhitespace.enabled": { + "type": "boolean", + "default": false, + "description": "Specifies whether or not to toggle whitespace off then showing blame annotations (*may* be required by certain fonts/themes)" + } + } + }, + "commands": [ + { + "command": "gitlens.diffDirectory", + "title": "Directory Compare", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithBranch", + "title": "Compare File with...", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithNext", + "title": "Compare File with Next Commit", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithPrevious", + "title": "Compare File with Previous", + "category": "GitLens" + }, + { + "command": "gitlens.diffLineWithPrevious", + "title": "Compare Line Commit with Previous", + "category": "GitLens" + }, + { + "command": "gitlens.diffWithWorking", + "title": "Compare File with Working Tree", + "category": "GitLens" + }, + { + "command": "gitlens.diffLineWithWorking", + "title": "Compare Line Commit with Working Tree", + "category": "GitLens" + }, + { + "command": "gitlens.showFileBlame", + "title": "Show File Blame Annotations", + "category": "GitLens" + }, + { + "command": "gitlens.showLineBlame", + "title": "Show Line Blame Annotations", + "category": "GitLens" + }, + { + "command": "gitlens.toggleFileBlame", + "title": "Toggle File Blame Annotations", + "category": "GitLens", + "icon": { + "dark": "images/git-icon-dark.svg", + "light": "images/git-icon-light.svg" + } + }, + { + "command": "gitlens.toggleLineBlame", + "title": "Toggle Line Blame Annotations", + "category": "GitLens" + }, + { + "command": "gitlens.toggleCodeLens", + "title": "Toggle Git Code Lens", + "category": "GitLens" + }, + { + "command": "gitlens.showBlameHistory", + "title": "Open Blame History Explorer", + "category": "GitLens" + }, + { + "command": "gitlens.showCommitSearch", + "title": "Search Commits", + "category": "GitLens" + }, + { + "command": "gitlens.showFileHistory", + "title": "Open File History Explorer", + "category": "GitLens" + }, + { + "command": "gitlens.showLastQuickPick", + "title": "Show Last Opened Quick Pick", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickCommitDetails", + "title": "Show Commit Details", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "title": "Show Line Commit Details", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickFileHistory", + "title": "Show File History", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickBranchHistory", + "title": "Show Branch History", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickRepoHistory", + "title": "Show Current Branch History", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickRepoStatus", + "title": "Show Repository Status", + "category": "GitLens" + }, + { + "command": "gitlens.showQuickStashList", + "title": "Show Stashed Changes", + "category": "GitLens" + }, + { + "command": "gitlens.copyShaToClipboard", + "title": "Copy Commit ID to Clipboard", + "category": "GitLens" + }, + { + "command": "gitlens.copyMessageToClipboard", + "title": "Copy Commit Message to Clipboard", + "category": "GitLens" + }, + { + "command": "gitlens.closeUnchangedFiles", + "title": "Close Unchanged Files", + "category": "GitLens" + }, + { + "command": "gitlens.openChangedFiles", + "title": "Open Changed Files", + "category": "GitLens" + }, + { + "command": "gitlens.openBranchInRemote", + "title": "Open Branch in Remote", + "category": "GitLens" + }, + { + "command": "gitlens.openCommitInRemote", + "title": "Open Line Commit in Remote", + "category": "GitLens" + }, + { + "command": "gitlens.openFileInRemote", + "title": "Open File in Remote", + "category": "GitLens" + }, + { + "command": "gitlens.openRepoInRemote", + "title": "Open Repository in Remote", + "category": "GitLens" + }, + { + "command": "gitlens.stashApply", + "title": "Apply Stashed Changes", + "category": "GitLens" + }, + { + "command": "gitlens.stashSave", + "title": "Stash Changes", + "category": "GitLens" + } + ], + "menus": { + "commandPalette": [ + { + "command": "gitlens.diffDirectory", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.diffWithBranch", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.diffWithNext", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.diffWithPrevious", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.diffLineWithPrevious", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.diffWithWorking", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.diffLineWithWorking", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.showFileBlame", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.showLineBlame", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.toggleFileBlame", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.toggleLineBlame", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.toggleCodeLens", + "when": "gitlens:isTracked && gitlens:canToggleCodeLens" + }, + { + "command": "gitlens.showBlameHistory", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.showFileHistory", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.showLastQuickPick", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickCommitDetails", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:isTracked" + }, + { + "command": "gitlens.showQuickBranchHistory", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickRepoHistory", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickRepoStatus", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickStashList", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.copyShaToClipboard", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.copyMessageToClipboard", + "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.closeUnchangedFiles", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.openChangedFiles", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.openBranchInRemote", + "when": "gitlens:hasRemotes" + }, + { + "command": "gitlens.openCommitInRemote", + "when": "gitlens:isBlameable && gitlens:hasRemotes" + }, + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:isTracked && gitlens:hasRemotes" + }, + { + "command": "gitlens.openRepoInRemote", + "when": "gitlens:hasRemotes" + }, + { + "command": "gitlens.stashApply", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.stashSave", + "when": "gitlens:enabled" + } + ], + "explorer/context": [ + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:enabled && config.gitlens.advanced.menus.explorerContext.remote", + "group": "navigation@100" + }, + { + "command": "gitlens.diffWithPrevious", + "when": "gitlens:enabled && config.gitlens.advanced.menus.explorerContext.fileDiff", + "group": "1_gitlens@1" + }, + { + "command": "gitlens.diffWithWorking", + "when": "gitlens:enabled && config.gitlens.advanced.menus.explorerContext.fileDiff", + "group": "1_gitlens@2" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:enabled && config.gitlens.advanced.menus.explorerContext.history", + "group": "1_gitlens_1@1" + } + ], + "editor/title": [ + { + "command": "gitlens.toggleFileBlame", + "when": "gitlens:isBlameable && config.gitlens.advanced.menus.editorTitle.blame", + "group": "navigation@100" + }, + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.remote", + "group": "1_gitlens" + }, + { + "command": "gitlens.openRepoInRemote", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.remote", + "group": "1_gitlens" + }, + { + "command": "gitlens.diffWithPrevious", + "when": "editorTextFocus && gitlens:isTracked && config.gitlens.advanced.menus.editorTitle.fileDiff", + "group": "2_gitlens" + }, + { + "command": "gitlens.diffWithWorking", + "when": "editorTextFocus && gitlens:isTracked && config.gitlens.advanced.menus.editorTitle.fileDiff", + "group": "2_gitlens" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "editorFocus && gitlens:isTracked && config.gitlens.advanced.menus.editorTitle.history", + "group": "2_gitlens_1" + }, + { + "command": "gitlens.showQuickRepoHistory", + "when": "!editorFocus && gitlens:enabled && config.gitlens.advanced.menus.editorTitle.history", + "group": "2_gitlens_1" + }, + { + "command": "gitlens.showQuickRepoStatus", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitle.status", + "group": "2_gitlens_1" + } + ], + "editor/title/context": [ + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.remote", + "group": "1_gitlens" + }, + { + "command": "gitlens.diffWithPrevious", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.fileDiff", + "group": "1_gitlens_1@1" + }, + { + "command": "gitlens.diffWithWorking", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.fileDiff", + "group": "1_gitlens_1@2" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.history", + "group": "1_gitlens_2@1" + }, + { + "command": "gitlens.toggleFileBlame", + "when": "gitlens:enabled && config.gitlens.advanced.menus.editorTitleContext.blame", + "group": "1_gitlens_2@2" + } + ], + "editor/context": [ + { + "command": "gitlens.openFileInRemote", + "when": "editorTextFocus && gitlens:isTracked && gitlens:hasRemotes && config.gitlens.advanced.menus.editorContext.remote", + "group": "navigation@100" + }, + { + "command": "gitlens.diffLineWithPrevious", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.lineDiff", + "group": "1_gitlens@1" + }, + { + "command": "gitlens.diffLineWithWorking", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.lineDiff", + "group": "1_gitlens@2" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.details", + "group": "1_gitlens@3" + }, + { + "command": "gitlens.diffWithPrevious", + "when": "editorTextFocus && gitlens:isTracked && config.gitlens.advanced.menus.editorContext.fileDiff", + "group": "1_gitlens_1@1" + }, + { + "command": "gitlens.diffWithWorking", + "when": "editorTextFocus && gitlens:isTracked && config.gitlens.advanced.menus.editorContext.fileDiff", + "group": "1_gitlens_1@2" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "gitlens:isTracked && config.gitlens.advanced.menus.editorContext.history", + "group": "3_gitlens@1" + }, + { + "command": "gitlens.toggleFileBlame", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.blame", + "group": "3_gitlens@2" + }, + { + "command": "gitlens.copyShaToClipboard", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.copy", + "group": "9_gitlens@1" + }, + { + "command": "gitlens.copyMessageToClipboard", + "when": "editorTextFocus && gitlens:isBlameable && config.gitlens.advanced.menus.editorContext.copy", + "group": "9_gitlens@2" + } + ] + }, + "keybindings": [ + { + "command": "gitlens.key.left", + "key": "alt+left", + "when": "gitlens:key:left" + }, + { + "command": "gitlens.key.right", + "key": "alt+right", + "when": "gitlens:key:right" + }, + { + "command": "gitlens.key.,", + "key": "alt+,", + "when": "gitlens:key:," + }, + { + "command": "gitlens.key..", + "key": "alt+.", + "when": "gitlens:key:." + }, + { + "command": "gitlens.toggleFileBlame", + "key": "alt+b", + "mac": "alt+b", + "when": "editorTextFocus && gitlens:isTracked" + }, + { + "command": "gitlens.toggleCodeLens", + "key": "shift+alt+b", + "mac": "shift+alt+b", + "when": "editorTextFocus && gitlens:isTracked && gitlens:canToggleCodeLens" + }, + { + "command": "gitlens.showLastQuickPick", + "key": "alt+-", + "mac": "alt+-", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showCommitSearch", + "key": "alt+/", + "mac": "alt+/", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickFileHistory", + "key": "alt+h", + "mac": "alt+h", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickRepoHistory", + "key": "shift+alt+h", + "mac": "shift+alt+h", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickRepoStatus", + "key": "alt+s", + "mac": "alt+s", + "when": "gitlens:enabled" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "key": "alt+c", + "mac": "alt+c", + "when": "editorTextFocus && gitlens:enabled" + }, + { + "command": "gitlens.diffWithNext", + "key": "alt+.", + "mac": "alt+.", + "when": "editorTextFocus && gitlens:isTracked" + }, + { + "command": "gitlens.diffLineWithPrevious", + "key": "shift+alt+,", + "mac": "shift+alt+,", + "when": "editorTextFocus && gitlens:isTracked" + }, + { + "command": "gitlens.diffWithPrevious", + "key": "alt+,", + "mac": "alt+,", + "when": "editorTextFocus && gitlens:isTracked" + }, + { + "command": "gitlens.diffLineWithWorking", + "key": "alt+w", + "mac": "alt+w", + "when": "editorTextFocus && gitlens:isTracked" + }, + { + "command": "gitlens.diffWithWorking", + "key": "shift+alt+w", + "mac": "shift+alt+w", + "when": "editorTextFocus && gitlens:isTracked" + } + ] + }, + "activationEvents": [ + "*" + ], + "scripts": { + "clean": "git clean -xdf", + "compile": "tslint --project tslint.json && tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "tslint --project tslint.json", + "pack": "git clean -xdf && vsce package", + "postinstall": "node ./node_modules/vscode/bin/install", + "pub": "git clean -xdf && vsce publish", + "reset": "git clean -xdf && npm install", + "vscode:prepublish": "npm install --no-save && npm run compile" + }, + "dependencies": { + "applicationinsights": "0.20.1", + "copy-paste": "1.3.0", + "iconv-lite": "0.4.17", + "ignore": "3.3.3", + "lodash.debounce": "4.0.8", + "lodash.escaperegexp": "4.1.2", + "lodash.isequal": "4.5.0", + "lodash.once": "4.1.1", + "moment": "2.18.1", + "spawn-rx": "2.0.11", + "tmp": "0.0.31" + }, + "devDependencies": { + "@types/copy-paste": "1.1.30", + "@types/iconv-lite": "0.0.1", + "@types/mocha": "2.2.41", + "@types/node": "7.0.28", + "@types/tmp": "0.0.33", + "mocha": "3.4.2", + "tslint": "5.4.3", + "typescript": "2.3.4", + "vscode": "1.1.0" + } +} diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts new file mode 100644 index 0000000..995513e --- /dev/null +++ b/src/annotations/annotationController.ts @@ -0,0 +1,282 @@ +'use strict'; +import { Functions, Objects } from '../system'; +import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; +import { AnnotationProviderBase } from './annotationProvider'; +import { TextDocumentComparer, TextEditorComparer } from '../comparers'; +import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration'; +import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; +import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; +import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; +import { Logger } from '../logger'; +import { WhitespaceController } from './whitespaceController'; + +export const Decorations = { + annotation: window.createTextEditorDecorationType({ + isWholeLine: true + } as DecorationRenderOptions), + highlight: undefined as TextEditorDecorationType | undefined +}; + +export class AnnotationController extends Disposable { + + private _onDidToggleAnnotations = new EventEmitter(); + get onDidToggleAnnotations(): Event { + return this._onDidToggleAnnotations.event; + } + + private _annotationsDisposable: Disposable | undefined; + private _annotationProviders: Map = new Map(); + private _config: IConfig; + private _disposable: Disposable; + private _whitespaceController: WhitespaceController | undefined; + + constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) { + super(() => this.dispose()); + + this._onConfigurationChanged(); + + const subscriptions: Disposable[] = []; + + subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + this._annotationProviders.forEach(async (p, i) => await this.clear(i)); + + Decorations.annotation && Decorations.annotation.dispose(); + Decorations.highlight && Decorations.highlight.dispose(); + + this._annotationsDisposable && this._annotationsDisposable.dispose(); + this._whitespaceController && this._whitespaceController.dispose(); + this._disposable && this._disposable.dispose(); + } + + private _onConfigurationChanged() { + let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get('enabled'); + if (!toggleWhitespace) { + // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures + // TODO: detect monospace font + toggleWhitespace = workspace.getConfiguration('editor').get('fontLigatures'); + } + + if (toggleWhitespace && !this._whitespaceController) { + this._whitespaceController = new WhitespaceController(); + } + else if (!toggleWhitespace && this._whitespaceController) { + this._whitespaceController.dispose(); + this._whitespaceController = undefined; + } + + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + const cfgHighlight = cfg.blame.file.lineHighlight; + const cfgTheme = cfg.theme.lineHighlight; + + let changed = false; + + if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) || + !Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) { + changed = true; + + Decorations.highlight && Decorations.highlight.dispose(); + + if (cfgHighlight.enabled) { + Decorations.highlight = window.createTextEditorDecorationType({ + gutterIconSize: 'contain', + isWholeLine: true, + overviewRulerLane: OverviewRulerLane.Right, + dark: { + backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line) + ? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor + : undefined, + gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter) + ? this.context.asAbsolutePath('images/blame-dark.svg') + : undefined, + overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler) + ? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor + : undefined + }, + light: { + backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line) + ? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor + : undefined, + gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter) + ? this.context.asAbsolutePath('images/blame-light.svg') + : undefined, + overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler) + ? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor + : undefined + } + }); + } + else { + Decorations.highlight = undefined; + } + } + + if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) || + !Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) || + !Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) { + changed = true; + } + + this._config = cfg; + + if (changed) { + // Since the configuration has changed -- reset any visible annotations + for (const provider of this._annotationProviders.values()) { + if (provider === undefined) continue; + + provider.reset(); + } + } + } + + async clear(column: number) { + const provider = this._annotationProviders.get(column); + if (!provider) return; + + this._annotationProviders.delete(column); + await provider.dispose(); + + if (this._annotationProviders.size === 0) { + Logger.log(`Remove listener registrations for annotations`); + this._annotationsDisposable && this._annotationsDisposable.dispose(); + this._annotationsDisposable = undefined; + } + + this._onDidToggleAnnotations.fire(); + } + + getAnnotationType(editor: TextEditor): FileAnnotationType | undefined { + const provider = this.getProvider(editor); + return provider === undefined ? undefined : provider.annotationType; + } + + getProvider(editor: TextEditor): AnnotationProviderBase | undefined { + if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined; + + return this._annotationProviders.get(editor.viewColumn || -1); + } + + async showAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise { + if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; + + const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); + if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) { + await currentProvider.selection(shaOrLine); + return true; + } + + const gitUri = await GitUri.fromUri(editor.document.uri, this.git); + + let provider: AnnotationProviderBase | undefined = undefined; + switch (type) { + case FileAnnotationType.Gutter: + provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri); + break; + case FileAnnotationType.Hover: + provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri); + break; + } + if (provider === undefined || !(await provider.validate())) return false; + + if (currentProvider) { + await this.clear(currentProvider.editor.viewColumn || -1); + } + + if (!this._annotationsDisposable && this._annotationProviders.size === 0) { + Logger.log(`Add listener registrations for annotations`); + + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); + subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); + subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this)); + subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); + subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); + + this._annotationsDisposable = Disposable.from(...subscriptions); + } + + this._annotationProviders.set(editor.viewColumn || -1, provider); + if (await provider.provideAnnotation(shaOrLine)) { + this._onDidToggleAnnotations.fire(); + return true; + } + return false; + } + + async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise { + if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; + + const provider = this._annotationProviders.get(editor.viewColumn || -1); + if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine); + + await this.clear(provider.editor.viewColumn || -1); + return false; + } + + private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + if (e.blameable || !e.editor) return; + + for (const [key, p] of this._annotationProviders) { + if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue; + + Logger.log('BlameabilityChanged:', `Clear annotations for column ${key}`); + this.clear(key); + } + } + + private _onTextDocumentChanged(e: TextDocumentChangeEvent) { + for (const [key, p] of this._annotationProviders) { + if (!TextDocumentComparer.equals(p.document, e.document)) continue; + + // We have to defer because isDirty is not reliable inside this event + setTimeout(() => { + // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it + if (e.document.isDirty) return; + + // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document + // Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking + Logger.log('TextDocumentChanged:', `Clear annotations for column ${key}`); + this.clear(key); + }, 1); + } + } + + private _onTextDocumentClosed(e: TextDocument) { + for (const [key, p] of this._annotationProviders) { + if (!TextDocumentComparer.equals(p.document, e)) continue; + + Logger.log('TextDocumentClosed:', `Clear annotations for column ${key}`); + this.clear(key); + } + } + + private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { + const viewColumn = e.viewColumn || -1; + + Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${viewColumn}`); + await this.clear(viewColumn); + + for (const [key, p] of this._annotationProviders) { + if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue; + + Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${key}`); + await this.clear(key); + } + } + + private async _onVisibleTextEditorsChanged(e: TextEditor[]) { + if (e.every(_ => _.document.uri.scheme === 'inmemory')) return; + + for (const [key, p] of this._annotationProviders) { + if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue; + + Logger.log('VisibleTextEditorsChanged:', `Clear annotations for column ${key}`); + this.clear(key); + } + } +} \ No newline at end of file diff --git a/src/annotations/annotationProvider.ts b/src/annotations/annotationProvider.ts new file mode 100644 index 0000000..e36168e --- /dev/null +++ b/src/annotations/annotationProvider.ts @@ -0,0 +1,74 @@ +'use strict'; +import { Functions } from '../system'; +import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { TextDocumentComparer } from '../comparers'; +import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { WhitespaceController } from './whitespaceController'; + + export abstract class AnnotationProviderBase extends Disposable { + + public annotationType: FileAnnotationType; + public document: TextDocument; + + protected _config: IConfig; + protected _disposable: Disposable; + + constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) { + super(() => this.dispose()); + + this.document = this.editor.document; + + this._config = workspace.getConfiguration().get(ExtensionKey)!; + + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + async dispose() { + await this.clear(); + + this._disposable && this._disposable.dispose(); + } + + private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) { + if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return; + + return this.selection(e.selections[0].active.line); + } + + async clear() { + if (this.editor !== undefined) { + try { + this.editor.setDecorations(this.decoration, []); + this.highlightDecoration && this.editor.setDecorations(this.highlightDecoration, []); + // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay + if (this.highlightDecoration !== undefined) { + await Functions.wait(1); + + if (this.highlightDecoration === undefined) return; + + this.editor.setDecorations(this.highlightDecoration, []); + } + } + catch (ex) { } + } + + // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace + this.whitespaceController && await this.whitespaceController.restore(); + } + + async reset() { + await this.clear(); + + this._config = workspace.getConfiguration().get(ExtensionKey)!; + + await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line); + } + + abstract async provideAnnotation(shaOrLine?: string | number): Promise; + abstract async selection(shaOrLine?: string | number): Promise; + abstract async validate(): Promise; + } \ No newline at end of file diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts new file mode 100644 index 0000000..6b71b38 --- /dev/null +++ b/src/annotations/annotations.ts @@ -0,0 +1,189 @@ +import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode'; +import { IThemeConfig, themeDefaults } from '../configuration'; +import { CommitFormatter, GitCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; +import * as moment from 'moment'; + +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; + +export class Annotations { + + static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) { + const color = this._getHeatmapColor(now, date); + (decoration.renderOptions!.before! as any).borderColor = color; + } + + private static _getHeatmapColor(now: moment.Moment, date: Date) { + const days = now.diff(moment(date), 'days'); + + 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 async changesHover(commit: GitCommit, line: number, uri: GitUri, git: GitService): Promise { + let message: string | undefined = undefined; + if (commit.isUncommitted) { + const [previous, current] = await git.getDiffForLine(uri, line + uri.offset); + message = CommitFormatter.toHoverDiff(commit, previous, current); + } + else if (commit.previousSha !== undefined) { + const [previous, current] = await git.getDiffForLine(uri, line + uri.offset, commit.previousSha); + message = CommitFormatter.toHoverDiff(commit, previous, current); + } + + return { + hoverMessage: message + } as DecorationOptions; + } + + static detailsHover(commit: GitCommit): DecorationOptions { + const message = CommitFormatter.toHoverAnnotation(commit); + return { + hoverMessage: message + } as DecorationOptions; + } + + static gutter(commit: GitCommit, format: string, dateFormatOrFormatOptions: string | null | ICommitFormatOptions, renderOptions: IRenderOptions, compact: boolean): DecorationOptions { + let content = `\u00a0${CommitFormatter.fromTemplate(format, commit, dateFormatOrFormatOptions)}\u00a0`; + if (compact) { + content = '\u00a0'.repeat(content.length); + } + + return { + renderOptions: { + before: { + ...renderOptions.before, + ...{ + contentText: content, + margin: '0 26px 0 0' + } + }, + 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: cfgFileTheme.separateLines ? 'calc(100% - 1px)' : '100%' + }, + dark: { + backgroundColor: cfgFileTheme.dark.backgroundColor || undefined, + color: cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor + } as DecorationInstanceRenderOptions, + light: { + backgroundColor: cfgFileTheme.light.backgroundColor || undefined, + color: cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor + } as DecorationInstanceRenderOptions + }; + } + + static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean): DecorationOptions { + return { + hoverMessage: CommitFormatter.toHoverAnnotation(commit), + 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: '\u200B', + height: cfgTheme.annotations.file.hover.separateLines ? 'calc(100% - 1px)' : '100%', + margin: '0 26px 0 0' + } + } as IRenderOptions; + } + + static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions { + const message = CommitFormatter.fromTemplate(format, commit, dateFormat); + return { + renderOptions: { + after: { + contentText: `\u00a0${message}\u00a0` + }, + 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 } }; + } +} \ No newline at end of file diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts new file mode 100644 index 0000000..8e02c47 --- /dev/null +++ b/src/annotations/blameAnnotationProvider.ts @@ -0,0 +1,82 @@ +'use strict'; +import { Iterables } from '../system'; +import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode'; +import { AnnotationProviderBase } from './annotationProvider'; +import { GitService, GitUri, IGitBlame } from '../gitService'; +import { WhitespaceController } from './whitespaceController'; + +export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase { + + protected _blame: Promise; + + constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) { + super(context, editor, decoration, highlightDecoration, whitespaceController); + + this._blame = this.git.getBlameForFile(this.uri); + } + + async selection(shaOrLine?: string | number, blame?: IGitBlame) { + if (!this.highlightDecoration) return; + + if (blame === undefined) { + blame = await this._blame; + if (!blame || !blame.lines.length) return; + } + + const offset = this.uri.offset; + + let sha: string | undefined = undefined; + if (typeof shaOrLine === 'string') { + sha = shaOrLine; + } + else if (typeof shaOrLine === 'number') { + const line = shaOrLine - offset; + if (line >= 0) { + const commitLine = blame.lines[line]; + sha = commitLine && commitLine.sha; + } + } + else { + sha = Iterables.first(blame.commits.values()).sha; + } + + if (!sha) { + this.editor.setDecorations(this.highlightDecoration, []); + return; + } + + const highlightDecorationRanges = blame.lines + .filter(l => l.sha === sha) + .map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000))); + + this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); + } + + async validate(): Promise { + const blame = await this._blame; + return blame !== undefined && blame.lines.length !== 0; + } + + protected async getBlame(requiresWhitespaceHack: boolean): Promise { + let whitespacePromise: Promise | undefined; + // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off) + if (requiresWhitespaceHack) { + whitespacePromise = this.whitespaceController && this.whitespaceController.override(); + } + + let blame: IGitBlame; + if (whitespacePromise) { + [blame] = await Promise.all([this._blame, whitespacePromise]); + } + else { + blame = await this._blame; + } + + if (!blame || !blame.lines.length) { + this.whitespaceController && await this.whitespaceController.restore(); + return undefined; + } + + return blame; + } +} \ No newline at end of file diff --git a/src/annotations/diffAnnotationProvider.ts b/src/annotations/diffAnnotationProvider.ts new file mode 100644 index 0000000..23db986 --- /dev/null +++ b/src/annotations/diffAnnotationProvider.ts @@ -0,0 +1,69 @@ +'use strict'; +import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; +import { AnnotationProviderBase } from './annotationProvider'; +import { GitService, GitUri } from '../gitService'; +import { WhitespaceController } from './whitespaceController'; + +export class DiffAnnotationProvider extends AnnotationProviderBase { + + constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, private git: GitService, private uri: GitUri) { + super(context, editor, decoration, highlightDecoration, whitespaceController); + } + + async provideAnnotation(shaOrLine?: string | number): Promise { + // let sha1: string | undefined = undefined; + // let sha2: string | undefined = undefined; + // if (shaOrLine === undefined) { + // const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); + // if (commit === undefined) return false; + + // sha1 = commit.previousSha; + // } + // else if (typeof shaOrLine === 'string') { + // sha1 = shaOrLine; + // } + // else { + // const blame = await this.git.getBlameForLine(this.uri, shaOrLine); + // if (blame === undefined) return false; + + // sha1 = blame.commit.previousSha; + // sha2 = blame.commit.sha; + // } + + // if (sha1 === undefined) return false; + + const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); + if (commit === undefined) return false; + + const diff = await this.git.getDiffForFile(this.uri, commit.previousSha); + if (diff === undefined) return false; + + const decorators: DecorationOptions[] = []; + + for (const chunk of diff.chunks) { + let count = chunk.currentStart - 2; + for (const change of chunk.current) { + if (change === undefined) continue; + + count++; + + if (change.state === 'unchanged') continue; + + decorators.push({ + range: new Range(new Position(count, 0), new Position(count, 0)) + } as DecorationOptions); + } + } + + this.editor.setDecorations(this.decoration, decorators); + + return true; + } + + async selection(shaOrLine?: string | number): Promise { + } + + async validate(): Promise { + return true; + } +} \ No newline at end of file diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts new file mode 100644 index 0000000..a125438 --- /dev/null +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -0,0 +1,76 @@ +'use strict'; +import { Strings } from '../system'; +import { DecorationOptions, Range } from 'vscode'; +import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; +import { Annotations, endOfLineIndex } from './annotations'; +import { FileAnnotationType } from '../configuration'; +import { ICommitFormatOptions } from '../gitService'; +import * as moment from 'moment'; + +export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { + + async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { + this.annotationType = FileAnnotationType.Gutter; + + const blame = await this.getBlame(true); + if (blame === undefined) return false; + + const cfg = this._config.annotations.file.gutter; + + // Precalculate the formatting options so we don't need to do it on each iteration + const tokenOptions = Strings.getTokensFromTemplate(cfg.format) + .reduce((map, token) => { + map[token.key] = token.options; + return map; + }, {} as { [token: string]: ICommitFormatOptions }); + + const options: ICommitFormatOptions = { + dateFormat: cfg.dateFormat, + tokenOptions: tokenOptions + }; + + const now = moment(); + const offset = this.uri.offset; + let previousLine: string | undefined = undefined; + const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap); + + const decorations: DecorationOptions[] = []; + + for (const l of blame.lines) { + const commit = blame.commits.get(l.sha); + if (commit === undefined) continue; + + const line = l.line + offset; + + const gutter = Annotations.gutter(commit, cfg.format, options, renderOptions, cfg.compact && previousLine === l.sha); + + if (cfg.compact) { + const isEmptyOrWhitespace = this.document.lineAt(line).isEmptyOrWhitespace; + previousLine = isEmptyOrWhitespace ? undefined : l.sha; + } + + if (cfg.heatmap.enabled) { + Annotations.applyHeatmap(gutter, commit.date, now); + } + + const firstNonWhitespace = this.editor.document.lineAt(line).firstNonWhitespaceCharacterIndex; + gutter.range = this.editor.document.validateRange(new Range(line, 0, line, firstNonWhitespace)); + decorations.push(gutter); + + if (cfg.hover.details) { + const details = Annotations.detailsHover(commit); + details.range = cfg.hover.wholeLine + ? this.editor.document.validateRange(new Range(line, 0, line, endOfLineIndex)) + : gutter.range; + decorations.push(details); + } + } + + if (decorations.length) { + this.editor.setDecorations(this.decoration, decorations); + } + + this.selection(shaOrLine, blame); + return true; + } +} \ No newline at end of file diff --git a/src/annotations/hoverBlameAnnotationProvider.ts b/src/annotations/hoverBlameAnnotationProvider.ts new file mode 100644 index 0000000..64694f0 --- /dev/null +++ b/src/annotations/hoverBlameAnnotationProvider.ts @@ -0,0 +1,49 @@ +'use strict'; +import { DecorationOptions, Range } from 'vscode'; +import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; +import { Annotations, endOfLineIndex } from './annotations'; +import { FileAnnotationType } from '../configuration'; +import * as moment from 'moment'; + +export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { + + async provideAnnotation(shaOrLine?: string | number): Promise { + this.annotationType = FileAnnotationType.Hover; + + const blame = await this.getBlame(this._config.annotations.file.hover.heatmap.enabled); + if (blame === undefined) return false; + + const cfg = this._config.annotations.file.hover; + + const now = moment(); + const offset = this.uri.offset; + const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap); + + const decorations: DecorationOptions[] = []; + + for (const l of blame.lines) { + const commit = blame.commits.get(l.sha); + if (commit === undefined) continue; + + const line = l.line + offset; + + const hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled); + + const endIndex = cfg.wholeLine ? endOfLineIndex : this.editor.document.lineAt(line).firstNonWhitespaceCharacterIndex; + hover.range = this.editor.document.validateRange(new Range(line, 0, line, endIndex)); + + if (cfg.heatmap.enabled) { + Annotations.applyHeatmap(hover, commit.date, now); + } + + decorations.push(hover); + } + + if (decorations.length) { + this.editor.setDecorations(this.decoration, decorations); + } + + this.selection(shaOrLine, blame); + return true; + } +} \ No newline at end of file diff --git a/src/whitespaceController.ts b/src/annotations/whitespaceController.ts similarity index 96% rename from src/whitespaceController.ts rename to src/annotations/whitespaceController.ts index 67a54ef..2a02145 100644 --- a/src/whitespaceController.ts +++ b/src/annotations/whitespaceController.ts @@ -1,6 +1,6 @@ 'use strict'; import { Disposable, workspace } from 'vscode'; -import { Logger } from './logger'; +import { Logger } from '../logger'; interface ConfigurationInspection { key: string; @@ -118,8 +118,6 @@ export class WhitespaceController extends Disposable { if (this._count === 1 && this._configuration.overrideRequired) { // Override whitespace (turn off) await this._overrideWhitespace(); - // Add a delay to give the editor time to turn off the whitespace - await new Promise((resolve, reject) => setTimeout(resolve, 250)); } } diff --git a/src/blameActiveLineController.ts b/src/blameActiveLineController.ts deleted file mode 100644 index a99da0b..0000000 --- a/src/blameActiveLineController.ts +++ /dev/null @@ -1,391 +0,0 @@ -'use strict'; -import { Functions, Objects } from './system'; -import { DecorationInstanceRenderOptions, DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; -import { BlameAnnotationController } from './blameAnnotationController'; -import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter'; -import { Commands } from './commands'; -import { TextEditorComparer } from './comparers'; -import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; -import { DocumentSchemes, ExtensionKey } from './constants'; -import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; -import * as moment from 'moment'; - -const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ - after: { - margin: '0 0 0 4em' - } -} as DecorationRenderOptions); - -export class BlameActiveLineController extends Disposable { - - private _activeEditorLineDisposable: Disposable | undefined; - private _blameable: boolean; - private _config: IConfig; - private _currentLine: number = -1; - private _disposable: Disposable; - private _editor: TextEditor | undefined; - private _statusBarItem: StatusBarItem | undefined; - private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise; - private _uri: GitUri; - - constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: BlameAnnotationController) { - super(() => this.dispose()); - - this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250); - - this._onConfigurationChanged(); - - const subscriptions: Disposable[] = []; - - subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); - subscriptions.push(git.onDidChangeGitCache(this._onGitCacheChanged, this)); - subscriptions.push(annotationController.onDidToggleBlameAnnotations(this._onBlameAnnotationToggled, this)); - - this._disposable = Disposable.from(...subscriptions); - } - - dispose() { - this._editor && this._editor.setDecorations(activeLineDecoration, []); - - this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose(); - this._statusBarItem && this._statusBarItem.dispose(); - this._disposable && this._disposable.dispose(); - } - - private _onConfigurationChanged() { - const cfg = workspace.getConfiguration().get(ExtensionKey)!; - - let changed = false; - - if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { - changed = true; - if (cfg.statusBar.enabled) { - const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; - if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) { - this._statusBarItem.dispose(); - this._statusBarItem = undefined; - } - - this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); - this._statusBarItem.command = cfg.statusBar.command; - } - else if (!cfg.statusBar.enabled && this._statusBarItem) { - this._statusBarItem.dispose(); - this._statusBarItem = undefined; - } - } - - if (!Objects.areEquivalent(cfg.blame.annotation.activeLine, this._config && this._config.blame.annotation.activeLine)) { - changed = true; - if (cfg.blame.annotation.activeLine !== 'off' && this._editor) { - this._editor.setDecorations(activeLineDecoration, []); - } - } - if (!Objects.areEquivalent(cfg.blame.annotation.activeLineDarkColor, this._config && this._config.blame.annotation.activeLineDarkColor) || - !Objects.areEquivalent(cfg.blame.annotation.activeLineLightColor, this._config && this._config.blame.annotation.activeLineLightColor)) { - changed = true; - } - - this._config = cfg; - - if (!changed) return; - - const trackActiveLine = cfg.statusBar.enabled || cfg.blame.annotation.activeLine !== 'off'; - if (trackActiveLine && !this._activeEditorLineDisposable) { - const subscriptions: Disposable[] = []; - - subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); - subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); - subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); - - this._activeEditorLineDisposable = Disposable.from(...subscriptions); - } - else if (!trackActiveLine && this._activeEditorLineDisposable) { - this._activeEditorLineDisposable.dispose(); - this._activeEditorLineDisposable = undefined; - } - - this._onActiveTextEditorChanged(window.activeTextEditor); - } - - private isEditorBlameable(editor: TextEditor | undefined): boolean { - if (editor === undefined || editor.document === undefined) return false; - - if (!this.git.isTrackable(editor.document.uri)) return false; - if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false; - - return this.git.isEditorBlameable(editor); - } - - private async _onActiveTextEditorChanged(editor: TextEditor | undefined) { - this._currentLine = -1; - - const previousEditor = this._editor; - previousEditor && previousEditor.setDecorations(activeLineDecoration, []); - - if (editor === undefined || !this.isEditorBlameable(editor)) { - this.clear(editor); - - this._editor = undefined; - - return; - } - - this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; - this._editor = editor; - this._uri = await GitUri.fromUri(editor.document.uri, this.git); - - const maxLines = this._config.advanced.caching.statusBar.maxLines; - // If caching is on and the file is small enough -- kick off a blame for the whole file - if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { - this.git.getBlameForFile(this._uri); - } - - this._updateBlameDebounced(editor.selection.active.line, editor); - } - - private _onBlameabilityChanged(e: BlameabilityChangeEvent) { - this._blameable = e.blameable; - if (!e.blameable || !this._editor) { - this.clear(e.editor); - return; - } - - // Make sure this is for the editor we are tracking - if (!TextEditorComparer.equals(this._editor, e.editor)) return; - - this._updateBlameDebounced(this._editor.selection.active.line, this._editor); - } - - private _onBlameAnnotationToggled() { - this._onActiveTextEditorChanged(window.activeTextEditor); - } - - private _onGitCacheChanged() { - this._onActiveTextEditorChanged(window.activeTextEditor); - } - - private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { - // Make sure this is for the editor we are tracking - if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; - - const line = e.selections[0].active.line; - if (line === this._currentLine) return; - this._currentLine = line; - - if (!this._uri && e.textEditor) { - this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git); - } - - this._updateBlameDebounced(line, e.textEditor); - } - - private async _updateBlame(line: number, editor: TextEditor) { - line = line - this._uri.offset; - - let commit: GitCommit | undefined = undefined; - let commitLine: IGitCommitLine | undefined = undefined; - // Since blame information isn't valid when there are unsaved changes -- don't show any status - if (this._blameable && line >= 0) { - const blameLine = await this.git.getBlameForLine(this._uri, line); - commitLine = blameLine === undefined ? undefined : blameLine.line; - commit = blameLine === undefined ? undefined : blameLine.commit; - } - - if (commit !== undefined && commitLine !== undefined) { - this.show(commit, commitLine, editor); - } - else { - this.clear(editor); - } - } - - clear(editor: TextEditor | undefined, previousEditor?: TextEditor) { - editor && editor.setDecorations(activeLineDecoration, []); - // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay - if (editor) { - setTimeout(() => editor.setDecorations(activeLineDecoration, []), 1); - } - - this._statusBarItem && this._statusBarItem.hide(); - } - - async show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { - // I have no idea why I need this protection -- but it happens - if (!editor.document) return; - - if (this._config.statusBar.enabled && this._statusBarItem !== undefined) { - switch (this._config.statusBar.date) { - case 'off': - this._statusBarItem.text = `$(git-commit) ${commit.author}`; - break; - case 'absolute': - const dateFormat = this._config.statusBar.dateFormat || 'MMMM Do, YYYY h:MMa'; - let date: string; - try { - date = moment(commit.date).format(dateFormat); - } catch (ex) { - date = moment(commit.date).format('MMMM Do, YYYY h:MMa'); - } - this._statusBarItem.text = `$(git-commit) ${commit.author}, ${date}`; - break; - default: - this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; - break; - } - - switch (this._config.statusBar.command) { - case StatusBarCommand.BlameAnnotate: - this._statusBarItem.tooltip = 'Toggle Blame Annotations'; - break; - case StatusBarCommand.ShowBlameHistory: - this._statusBarItem.tooltip = 'Open Blame History Explorer'; - break; - case StatusBarCommand.ShowFileHistory: - this._statusBarItem.tooltip = 'Open File History Explorer'; - break; - case StatusBarCommand.DiffWithPrevious: - this._statusBarItem.command = Commands.DiffLineWithPrevious; - this._statusBarItem.tooltip = 'Compare File with Previous'; - break; - case StatusBarCommand.DiffWithWorking: - this._statusBarItem.command = Commands.DiffLineWithWorking; - this._statusBarItem.tooltip = 'Compare File with Working Tree'; - break; - case StatusBarCommand.ToggleCodeLens: - this._statusBarItem.tooltip = 'Toggle Git CodeLens'; - break; - case StatusBarCommand.ShowQuickCommitDetails: - this._statusBarItem.tooltip = 'Show Commit Details'; - break; - case StatusBarCommand.ShowQuickCommitFileDetails: - this._statusBarItem.tooltip = 'Show Line Commit Details'; - break; - case StatusBarCommand.ShowQuickFileHistory: - this._statusBarItem.tooltip = 'Show File History'; - break; - case StatusBarCommand.ShowQuickCurrentBranchHistory: - this._statusBarItem.tooltip = 'Show Branch History'; - break; - } - - this._statusBarItem.show(); - } - - if (this._config.blame.annotation.activeLine !== 'off') { - const activeLine = this._config.blame.annotation.activeLine; - const offset = this._uri.offset; - - const cfg = { - annotation: { - sha: true, - author: this._config.statusBar.enabled ? false : this._config.blame.annotation.author, - date: this._config.statusBar.enabled ? 'off' : this._config.blame.annotation.date, - message: true - } - } as IBlameConfig; - - const annotation = BlameAnnotationFormatter.getAnnotation(cfg, commit, BlameAnnotationFormat.Unconstrained); - - // Get the full commit message -- since blame only returns the summary - let logCommit: GitCommit | undefined = undefined; - if (!commit.isUncommitted) { - logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha); - } - - // I have no idea why I need this protection -- but it happens - if (!editor.document) return; - - let hoverMessage: string | string[] | undefined = undefined; - if (activeLine !== 'inline') { - // If the messages match (or we couldn't find the log), then this is a possible duplicate annotation - const possibleDuplicate = !logCommit || logCommit.message === commit.message; - // If we don't have a possible dupe or we aren't showing annotations get the hover message - if (!commit.isUncommitted && (!possibleDuplicate || !this.annotationController.isAnnotating(editor))) { - hoverMessage = BlameAnnotationFormatter.getAnnotationHover(cfg, blameLine, logCommit || commit); - - if (commit.previousSha !== undefined) { - const changes = await this.git.getDiffForLine(this._uri, blameLine.line + offset, commit.previousSha); - if (changes !== undefined) { - let previous = changes[0]; - if (previous !== undefined) { - previous = previous.replace(/\n/g, '\`\n>\n> \`').trim(); - hoverMessage += `\n\n---\n\`\`\`\n${previous}\n\`\`\``; - } - } - } - } - else if (commit.isUncommitted) { - const changes = await this.git.getDiffForLine(this._uri, blameLine.line + offset); - if (changes !== undefined) { - let previous = changes[0]; - if (previous !== undefined) { - previous = previous.replace(/\n/g, '\`\n>\n> \`').trim(); - hoverMessage = `\`${'0'.repeat(8)}\`   __Uncommitted change__\n\n---\n\`\`\`\n${previous}\n\`\`\``; - } - } - } - } - - let decorationOptions: [DecorationOptions] | undefined = undefined; - switch (activeLine) { - case 'both': - case 'inline': - const range = editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)); - decorationOptions = [ - { - range: range.with({ - start: range.start.with({ - character: range.end.character - }) - }), - hoverMessage: hoverMessage, - renderOptions: { - after: { - contentText: annotation - }, - dark: { - after: { - color: this._config.blame.annotation.activeLineDarkColor || 'rgba(153, 153, 153, 0.35)' - } - }, - light: { - after: { - color: this._config.blame.annotation.activeLineLightColor || 'rgba(153, 153, 153, 0.35)' - } - } - } as DecorationInstanceRenderOptions - } as DecorationOptions - ]; - - if (activeLine === 'both') { - // Add a hover decoration to the area between the start of the line and the first non-whitespace character - decorationOptions.push({ - range: range.with({ - end: range.end.with({ - character: editor.document.lineAt(range.end.line).firstNonWhitespaceCharacterIndex - }) - }), - hoverMessage: hoverMessage - } as DecorationOptions); - } - - break; - - case 'hover': - decorationOptions = [ - { - range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), - hoverMessage: hoverMessage - } as DecorationOptions - ]; - - break; - } - - if (decorationOptions !== undefined) { - editor.setDecorations(activeLineDecoration, decorationOptions); - } - } - } -} \ No newline at end of file diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts deleted file mode 100644 index 5cc80b8..0000000 --- a/src/blameAnnotationController.ts +++ /dev/null @@ -1,271 +0,0 @@ -'use strict'; -import { Functions } from './system'; -import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; -import { BlameAnnotationProvider } from './blameAnnotationProvider'; -import { TextDocumentComparer, TextEditorComparer } from './comparers'; -import { IBlameConfig } from './configuration'; -import { ExtensionKey } from './constants'; -import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from './gitService'; -import { Logger } from './logger'; -import { WhitespaceController } from './whitespaceController'; - -export const BlameDecorations = { - annotation: window.createTextEditorDecorationType({ - before: { - margin: '0 1.75em 0 0' - }, - after: { - margin: '0 0 0 4em' - } - } as DecorationRenderOptions), - highlight: undefined as TextEditorDecorationType | undefined -}; - -export class BlameAnnotationController extends Disposable { - - private _onDidToggleBlameAnnotations = new EventEmitter(); - get onDidToggleBlameAnnotations(): Event { - return this._onDidToggleBlameAnnotations.event; - } - - private _annotationProviders: Map = new Map(); - private _blameAnnotationsDisposable: Disposable | undefined; - private _config: IBlameConfig; - private _disposable: Disposable; - private _whitespaceController: WhitespaceController | undefined; - - constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) { - super(() => this.dispose()); - - this._onConfigurationChanged(); - - const subscriptions: Disposable[] = []; - - subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); - - this._disposable = Disposable.from(...subscriptions); - } - - dispose() { - this._annotationProviders.forEach(async (p, i) => await this.clear(i)); - - BlameDecorations.annotation && BlameDecorations.annotation.dispose(); - BlameDecorations.highlight && BlameDecorations.highlight.dispose(); - - this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); - this._whitespaceController && this._whitespaceController.dispose(); - this._disposable && this._disposable.dispose(); - } - - private _onConfigurationChanged() { - let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get('enabled'); - if (!toggleWhitespace) { - // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures - // TODO: detect monospace font - toggleWhitespace = workspace.getConfiguration('editor').get('fontLigatures'); - } - - if (toggleWhitespace && !this._whitespaceController) { - this._whitespaceController = new WhitespaceController(); - } - else if (!toggleWhitespace && this._whitespaceController) { - this._whitespaceController.dispose(); - this._whitespaceController = undefined; - } - - const cfg = workspace.getConfiguration(ExtensionKey).get('blame')!; - - if (cfg.annotation.highlight !== (this._config && this._config.annotation.highlight)) { - BlameDecorations.highlight && BlameDecorations.highlight.dispose(); - - switch (cfg.annotation.highlight) { - case 'gutter': - BlameDecorations.highlight = window.createTextEditorDecorationType({ - dark: { - gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), - overviewRulerColor: 'rgba(255, 255, 255, 0.75)' - }, - light: { - gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'), - overviewRulerColor: 'rgba(0, 0, 0, 0.75)' - }, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Right - }); - break; - - case 'line': - BlameDecorations.highlight = window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(255, 255, 255, 0.15)', - overviewRulerColor: 'rgba(255, 255, 255, 0.75)' - }, - light: { - backgroundColor: 'rgba(0, 0, 0, 0.15)', - overviewRulerColor: 'rgba(0, 0, 0, 0.75)' - }, - overviewRulerLane: OverviewRulerLane.Right, - isWholeLine: true - }); - break; - - case 'both': - BlameDecorations.highlight = window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(255, 255, 255, 0.15)', - gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), - overviewRulerColor: 'rgba(255, 255, 255, 0.75)' - }, - light: { - backgroundColor: 'rgba(0, 0, 0, 0.15)', - gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'), - overviewRulerColor: 'rgba(0, 0, 0, 0.75)' - }, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Right, - isWholeLine: true - }); - break; - - default: - BlameDecorations.highlight = undefined; - break; - } - } - - this._config = cfg; - } - - async clear(column: number) { - const provider = this._annotationProviders.get(column); - if (!provider) return; - - this._annotationProviders.delete(column); - await provider.dispose(); - - if (this._annotationProviders.size === 0) { - Logger.log(`Remove listener registrations for blame annotations`); - this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); - this._blameAnnotationsDisposable = undefined; - } - - this._onDidToggleBlameAnnotations.fire(); - } - - async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { - if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; - - const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); - if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) { - await currentProvider.setSelection(shaOrLine); - return true; - } - - const gitUri = await GitUri.fromUri(editor.document.uri, this.git); - const provider = new BlameAnnotationProvider(this.context, this.git, this._whitespaceController, editor, gitUri); - if (!await provider.supportsBlame()) return false; - - if (currentProvider) { - await this.clear(currentProvider.editor.viewColumn || -1); - } - - if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) { - Logger.log(`Add listener registrations for blame annotations`); - - const subscriptions: Disposable[] = []; - - subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); - subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); - subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this)); - subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); - subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); - - this._blameAnnotationsDisposable = Disposable.from(...subscriptions); - } - - this._annotationProviders.set(editor.viewColumn || -1, provider); - if (await provider.provideBlameAnnotation(shaOrLine)) { - this._onDidToggleBlameAnnotations.fire(); - return true; - } - return false; - } - - isAnnotating(editor: TextEditor): boolean { - if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; - - return !!this._annotationProviders.get(editor.viewColumn || -1); - } - - async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { - if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; - - const provider = this._annotationProviders.get(editor.viewColumn || -1); - if (!provider) return this.showBlameAnnotation(editor, shaOrLine); - - await this.clear(provider.editor.viewColumn || -1); - return false; - } - - private _onBlameabilityChanged(e: BlameabilityChangeEvent) { - if (e.blameable || !e.editor) return; - - for (const [key, p] of this._annotationProviders) { - if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue; - - Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`); - this.clear(key); - } - } - - private _onTextDocumentChanged(e: TextDocumentChangeEvent) { - for (const [key, p] of this._annotationProviders) { - if (!TextDocumentComparer.equals(p.document, e.document)) continue; - - // We have to defer because isDirty is not reliable inside this event - setTimeout(() => { - // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it - if (e.document.isDirty) return; - - // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document - // Which means the document has been reloaded and the blame annotations have been removed, so we need to update (clear) our state tracking - Logger.log('TextDocumentChanged:', `Clear blame annotations for column ${key}`); - this.clear(key); - }, 1); - } - } - - private _onTextDocumentClosed(e: TextDocument) { - for (const [key, p] of this._annotationProviders) { - if (!TextDocumentComparer.equals(p.document, e)) continue; - - Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`); - this.clear(key); - } - } - - private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { - const viewColumn = e.viewColumn || -1; - - Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`); - await this.clear(viewColumn); - - for (const [key, p] of this._annotationProviders) { - if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue; - - Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`); - await this.clear(key); - } - } - - private async _onVisibleTextEditorsChanged(e: TextEditor[]) { - if (e.every(_ => _.document.uri.scheme === 'inmemory')) return; - - for (const [key, p] of this._annotationProviders) { - if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue; - - Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`); - this.clear(key); - } - } -} \ No newline at end of file diff --git a/src/blameAnnotationFormatter.ts b/src/blameAnnotationFormatter.ts deleted file mode 100644 index 007383b..0000000 --- a/src/blameAnnotationFormatter.ts +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; -import { IBlameConfig } from './configuration'; -import { GitCommit, IGitCommitLine } from './gitService'; -import * as moment from 'moment'; - -export const defaultAbsoluteDateLength = 10; -export const defaultRelativeDateLength = 13; -export const defaultAuthorLength = 16; -export const defaultMessageLength = 32; - -export enum BlameAnnotationFormat { - Constrained, - Unconstrained -} - -export class BlameAnnotationFormatter { - - static getAnnotation(config: IBlameConfig, commit: GitCommit, format: BlameAnnotationFormat) { - const sha = commit.shortSha; - let message = this.getMessage(config, commit, format === BlameAnnotationFormat.Unconstrained ? 0 : defaultMessageLength); - - if (format === BlameAnnotationFormat.Unconstrained) { - const authorAndDate = this.getAuthorAndDate(config, commit, config.annotation.dateFormat || 'MMMM Do, YYYY h:MMa'); - if (config.annotation.sha) { - message = `${sha}${(authorAndDate ? `\u00a0\u2022\u00a0${authorAndDate}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`; - } - else if (config.annotation.author || config.annotation.date) { - message = `${authorAndDate}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`; - } - - return message; - } - - const author = this.getAuthor(config, commit, defaultAuthorLength); - const date = this.getDate(config, commit, config.annotation.dateFormat || 'MM/DD/YYYY', true); - if (config.annotation.sha) { - message = `${sha}${(author ? `\u00a0\u2022\u00a0${author}` : '')}${(date ? `\u00a0\u2022\u00a0${date}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`; - } - else if (config.annotation.author) { - message = `${author}${(date ? `\u00a0\u2022\u00a0${date}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`; - } - else if (config.annotation.date) { - message = `${date}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`; - } - - return message; - } - - static getAnnotationHover(config: IBlameConfig, line: IGitCommitLine, commit: GitCommit): string | string[] { - const message = `> \`${commit.message.replace(/\n/g, '\`\n>\n> \`')}\``; - if (commit.isUncommitted) { - return `\`${'0'.repeat(8)}\`   __Uncommitted change__`; - } - - return `\`${commit.shortSha}\`   __${commit.author}__, ${moment(commit.date).fromNow()} _(${moment(commit.date).format(config.annotation.dateFormat || 'MMMM Do, YYYY h:MMa')})_ \n\n${message}`; - } - - static getAuthorAndDate(config: IBlameConfig, commit: GitCommit, format: string, force: boolean = false) { - if (!force && !config.annotation.author && (!config.annotation.date || config.annotation.date === 'off')) return ''; - - if (!config.annotation.author) { - return this.getDate(config, commit, format); - } - - if (!config.annotation.date || config.annotation.date === 'off') { - return this.getAuthor(config, commit); - } - - return `${this.getAuthor(config, commit)}, ${this.getDate(config, commit, format)}`; - } - - static getAuthor(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) { - if (!force && !config.annotation.author) return ''; - - const author = commit.isUncommitted ? 'Uncommitted' : commit.author; - if (!truncateTo) return author; - - if (author.length > truncateTo) { - return `${author.substring(0, truncateTo - 1)}\u2026`; - } - - if (force) return author; // Don't pad when just asking for the value - return author + '\u00a0'.repeat(truncateTo - author.length); - } - - static getDate(config: IBlameConfig, commit: GitCommit, format: string, truncate: boolean = false, force: boolean = false) { - if (!force && (!config.annotation.date || config.annotation.date === 'off')) return ''; - - const date = config.annotation.date === 'relative' - ? moment(commit.date).fromNow() - : moment(commit.date).format(format); - if (!truncate) return date; - - const truncateTo = config.annotation.date === 'relative' ? defaultRelativeDateLength : defaultAbsoluteDateLength; - if (date.length > truncateTo) { - return `${date.substring(0, truncateTo - 1)}\u2026`; - } - - if (force) return date; // Don't pad when just asking for the value - return date + '\u00a0'.repeat(truncateTo - date.length); - } - - static getMessage(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) { - if (!force && !config.annotation.message) return ''; - - const message = commit.isUncommitted ? 'Uncommitted change' : commit.message; - if (truncateTo && message.length > truncateTo) { - return `${message.substring(0, truncateTo - 1)}\u2026`; - } - - return message; - } -} \ No newline at end of file diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts deleted file mode 100644 index 0e9bba1..0000000 --- a/src/blameAnnotationProvider.ts +++ /dev/null @@ -1,302 +0,0 @@ -'use strict'; -import { Iterables } from './system'; -import { DecorationInstanceRenderOptions, DecorationOptions, Disposable, ExtensionContext, Range, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; -import { BlameAnnotationFormat, BlameAnnotationFormatter, defaultAuthorLength } from './blameAnnotationFormatter'; -import { BlameDecorations } from './blameAnnotationController'; -import { TextDocumentComparer } from './comparers'; -import { BlameAnnotationStyle, IBlameConfig } from './configuration'; -import { ExtensionKey } from './constants'; -import { GitService, GitUri, IGitBlame } from './gitService'; -import { WhitespaceController } from './whitespaceController'; - -export class BlameAnnotationProvider extends Disposable { - - public document: TextDocument; - - private _blame: Promise; - private _config: IBlameConfig; - private _disposable: Disposable; - - constructor(context: ExtensionContext, private git: GitService, private whitespaceController: WhitespaceController | undefined, public editor: TextEditor, private uri: GitUri) { - super(() => this.dispose()); - - this.document = this.editor.document; - - this._blame = this.git.getBlameForFile(this.uri); - - this._config = workspace.getConfiguration(ExtensionKey).get('blame')!; - - const subscriptions: Disposable[] = []; - - subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); - - this._disposable = Disposable.from(...subscriptions); - } - - async dispose() { - if (this.editor) { - try { - this.editor.setDecorations(BlameDecorations.annotation, []); - BlameDecorations.highlight && this.editor.setDecorations(BlameDecorations.highlight, []); - // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay - if (BlameDecorations.highlight !== undefined) { - setTimeout(() => { - if (BlameDecorations.highlight === undefined) return; - - this.editor.setDecorations(BlameDecorations.highlight, []); - }, 1); - } - } - catch (ex) { } - } - - // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace - this.whitespaceController && await this.whitespaceController.restore(); - - this._disposable && this._disposable.dispose(); - } - - private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) { - if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return; - - return this.setSelection(e.selections[0].active.line); - } - - async supportsBlame(): Promise { - const blame = await this._blame; - return !!(blame && blame.lines.length); - } - - async provideBlameAnnotation(shaOrLine?: string | number): Promise { - let whitespacePromise: Promise | undefined; - // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off) - if (this._config.annotation.style !== BlameAnnotationStyle.Trailing) { - whitespacePromise = this.whitespaceController && this.whitespaceController.override(); - } - - let blame: IGitBlame; - if (whitespacePromise) { - [blame] = await Promise.all([this._blame, whitespacePromise]); - } - else { - blame = await this._blame; - } - - if (!blame || !blame.lines.length) { - this.whitespaceController && await this.whitespaceController.restore(); - return false; - } - - let blameDecorationOptions: DecorationOptions[] | undefined; - switch (this._config.annotation.style) { - case BlameAnnotationStyle.Compact: - blameDecorationOptions = this._getCompactGutterDecorations(blame); - break; - case BlameAnnotationStyle.Expanded: - blameDecorationOptions = this._getExpandedGutterDecorations(blame, false); - break; - case BlameAnnotationStyle.Trailing: - blameDecorationOptions = this._getExpandedGutterDecorations(blame, true); - break; - } - - if (blameDecorationOptions) { - this.editor.setDecorations(BlameDecorations.annotation, blameDecorationOptions); - } - - this._setSelection(blame, shaOrLine); - return true; - } - - async setSelection(shaOrLine?: string | number) { - const blame = await this._blame; - if (!blame || !blame.lines.length) return; - - return this._setSelection(blame, shaOrLine); - } - - private _setSelection(blame: IGitBlame, shaOrLine?: string | number) { - if (!BlameDecorations.highlight) return; - - const offset = this.uri.offset; - - let sha: string | undefined = undefined; - if (typeof shaOrLine === 'string') { - sha = shaOrLine; - } - else if (typeof shaOrLine === 'number') { - const line = shaOrLine - offset; - if (line >= 0) { - const commitLine = blame.lines[line]; - sha = commitLine && commitLine.sha; - } - } - else { - sha = Iterables.first(blame.commits.values()).sha; - } - - if (!sha) { - this.editor.setDecorations(BlameDecorations.highlight, []); - return; - } - - const highlightDecorationRanges = blame.lines - .filter(l => l.sha === sha) - .map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000))); - - this.editor.setDecorations(BlameDecorations.highlight, highlightDecorationRanges); - } - - private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] { - const offset = this.uri.offset; - - let count = 0; - let lastSha: string; - return blame.lines.map(l => { - const commit = blame.commits.get(l.sha); - if (commit === undefined) throw new Error(`Cannot find sha ${l.sha}`); - - let color: string; - if (commit.isUncommitted) { - color = 'rgba(0, 188, 242, 0.6)'; - } - else { - color = l.previousSha ? '#999999' : '#6b6b6b'; - } - - let gutter = ''; - if (lastSha !== l.sha) { - count = -1; - } - - const isEmptyOrWhitespace = this.document.lineAt(l.line).isEmptyOrWhitespace; - if (!isEmptyOrWhitespace) { - switch (++count) { - case 0: - gutter = commit.shortSha; - break; - case 1: - gutter = `\u2759 ${BlameAnnotationFormatter.getAuthor(this._config, commit, defaultAuthorLength, true)}`; - break; - case 2: - gutter = `\u2759 ${BlameAnnotationFormatter.getDate(this._config, commit, this._config.annotation.dateFormat || 'MM/DD/YYYY', true, true)}`; - break; - default: - gutter = `\u2759`; - break; - } - } - - const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit); - - lastSha = l.sha; - - return { - range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)), - hoverMessage: hoverMessage, - renderOptions: { - before: { - color: color, - contentText: gutter, - width: '11em' - } - } - } as DecorationOptions; - }); - } - - private _getExpandedGutterDecorations(blame: IGitBlame, trailing: boolean = false): DecorationOptions[] { - const offset = this.uri.offset; - - let width = 0; - if (!trailing) { - if (this._config.annotation.sha) { - width += 5; - } - if (this._config.annotation.date && this._config.annotation.date !== 'off') { - if (width > 0) { - width += 7; - } - else { - width += 6; - } - - if (this._config.annotation.date === 'relative') { - width += 2; - } - } - if (this._config.annotation.author) { - if (width > 5 + 6) { - width += 12; - } - else if (width > 0) { - width += 11; - } - else { - width += 10; - } - } - if (this._config.annotation.message) { - if (width > 5 + 6 + 10) { - width += 21; - } - else if (width > 5 + 6) { - width += 21; - } - else if (width > 0) { - width += 21; - } - else { - width += 19; - } - } - } - - return blame.lines.map(l => { - const commit = blame.commits.get(l.sha); - if (commit === undefined) throw new Error(`Cannot find sha ${l.sha}`); - - let color: string; - if (commit.isUncommitted) { - color = 'rgba(0, 188, 242, 0.6)'; - } - else { - if (trailing) { - color = l.previousSha ? 'rgba(153, 153, 153, 0.5)' : 'rgba(107, 107, 107, 0.5)'; - } - else { - color = l.previousSha ? 'rgb(153, 153, 153)' : 'rgb(107, 107, 107)'; - } - } - - const format = trailing ? BlameAnnotationFormat.Unconstrained : BlameAnnotationFormat.Constrained; - const gutter = BlameAnnotationFormatter.getAnnotation(this._config, commit, format); - const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit); - - let renderOptions: DecorationInstanceRenderOptions; - if (trailing) { - renderOptions = { - after: { - color: color, - contentText: gutter - } - } as DecorationInstanceRenderOptions; - } - else { - renderOptions = { - before: { - color: color, - contentText: gutter, - width: `${width}em` - } - } as DecorationInstanceRenderOptions; - } - - return { - range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)), - hoverMessage: hoverMessage, - renderOptions: renderOptions - } as DecorationOptions; - }); - } -} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 2f33c49..a84e773 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -19,10 +19,11 @@ export * from './commands/openCommitInRemote'; export * from './commands/openFileInRemote'; export * from './commands/openInRemote'; export * from './commands/openRepoInRemote'; -export * from './commands/showBlame'; export * from './commands/showBlameHistory'; +export * from './commands/showFileBlame'; export * from './commands/showFileHistory'; export * from './commands/showLastQuickPick'; +export * from './commands/showLineBlame'; export * from './commands/showQuickCommitDetails'; export * from './commands/showQuickCommitFileDetails'; export * from './commands/showCommitSearch'; @@ -34,5 +35,6 @@ export * from './commands/showQuickStashList'; export * from './commands/stashApply'; export * from './commands/stashDelete'; export * from './commands/stashSave'; -export * from './commands/toggleBlame'; -export * from './commands/toggleCodeLens'; \ No newline at end of file +export * from './commands/toggleCodeLens'; +export * from './commands/toggleFileBlame'; +export * from './commands/toggleLineBlame'; \ No newline at end of file diff --git a/src/commands/common.ts b/src/commands/common.ts index e850379..ae6c56b 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -7,13 +7,13 @@ import { Telemetry } from '../telemetry'; export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | 'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.openChangedFiles' | 'gitlens.openBranchInRemote' | 'gitlens.openCommitInRemote' | 'gitlens.openFileInRemote' | 'gitlens.openInRemote' | 'gitlens.openRepoInRemote' | - 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileHistory' | - 'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' | + 'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileBlame' | 'gitlens.showFileHistory' | + 'gitlens.showLastQuickPick' | 'gitlens.showLineBlame' | 'gitlens.showQuickBranchHistory' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' | 'gitlens.showQuickRepoStatus' | 'gitlens.showQuickStashList' | 'gitlens.stashApply' | 'gitlens.stashDelete' | 'gitlens.stashSave' | - 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; + 'gitlens.toggleCodeLens' | 'gitlens.toggleFileBlame' | 'gitlens.toggleLineBlame'; export const Commands = { CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands, CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands, @@ -31,7 +31,8 @@ export const Commands = { OpenFileInRemote: 'gitlens.openFileInRemote' as Commands, OpenInRemote: 'gitlens.openInRemote' as Commands, OpenRepoInRemote: 'gitlens.openRepoInRemote' as Commands, - ShowBlame: 'gitlens.showBlame' as Commands, + ShowFileBlame: 'gitlens.showFileBlame' as Commands, + ShowLineBlame: 'gitlens.showLineBlame' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, ShowCommitSearch: 'gitlens.showCommitSearch' as Commands, ShowFileHistory: 'gitlens.showFileHistory' as Commands, @@ -46,7 +47,8 @@ export const Commands = { StashApply: 'gitlens.stashApply' as Commands, StashDelete: 'gitlens.stashDelete' as Commands, StashSave: 'gitlens.stashSave' as Commands, - ToggleBlame: 'gitlens.toggleBlame' as Commands, + ToggleFileBlame: 'gitlens.toggleFileBlame' as Commands, + ToggleLineBlame: 'gitlens.toggleLineBlame' as Commands, ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands }; diff --git a/src/commands/showBlame.ts b/src/commands/showBlame.ts deleted file mode 100644 index efb78d7..0000000 --- a/src/commands/showBlame.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { BlameAnnotationController } from '../blameAnnotationController'; -import { Commands, EditorCommand } from './common'; -import { Logger } from '../logger'; - -export interface ShowBlameCommandArgs { - sha?: string; -} - -export class ShowBlameCommand extends EditorCommand { - - constructor(private annotationController: BlameAnnotationController) { - super(Commands.ShowBlame); - } - - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowBlameCommandArgs = {}): Promise { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - - try { - return this.annotationController.showBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line); - } - catch (ex) { - Logger.error(ex, 'ShowBlameCommand'); - return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); - } - } -} \ No newline at end of file diff --git a/src/commands/showFileBlame.ts b/src/commands/showFileBlame.ts new file mode 100644 index 0000000..d18d24c --- /dev/null +++ b/src/commands/showFileBlame.ts @@ -0,0 +1,35 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; +import { AnnotationController } from '../annotations/annotationController'; +import { Commands, EditorCommand } from './common'; +import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { Logger } from '../logger'; + +export interface ShowFileBlameCommandArgs { + sha?: string; + type?: FileAnnotationType; +} + +export class ShowFileBlameCommand extends EditorCommand { + + constructor(private annotationController: AnnotationController) { + super(Commands.ShowFileBlame); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; + + try { + if (args.type === undefined) { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + args.type = cfg.blame.file.annotationType; + } + + return this.annotationController.showAnnotations(editor, args.type, args.sha !== undefined ? args.sha : editor.selection.active.line); + } + catch (ex) { + Logger.error(ex, 'ShowFileBlameCommand'); + return window.showErrorMessage(`Unable to show file blame annotations. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/showLineBlame.ts b/src/commands/showLineBlame.ts new file mode 100644 index 0000000..340a559 --- /dev/null +++ b/src/commands/showLineBlame.ts @@ -0,0 +1,34 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; +import { CurrentLineController } from '../currentLineController'; +import { Commands, EditorCommand } from './common'; +import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration'; +import { Logger } from '../logger'; + +export interface ShowLineBlameCommandArgs { + type?: LineAnnotationType; +} + +export class ShowLineBlameCommand extends EditorCommand { + + constructor(private currentLineController: CurrentLineController) { + super(Commands.ShowLineBlame); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; + + try { + if (args.type === undefined) { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + args.type = cfg.blame.line.annotationType; + } + + return this.currentLineController.showAnnotations(editor, args.type); + } + catch (ex) { + Logger.error(ex, 'ShowLineBlameCommand'); + return window.showErrorMessage(`Unable to show line blame annotations. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/toggleBlame.ts b/src/commands/toggleBlame.ts deleted file mode 100644 index b26d053..0000000 --- a/src/commands/toggleBlame.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; -import { BlameAnnotationController } from '../blameAnnotationController'; -import { Commands, EditorCommand } from './common'; -import { Logger } from '../logger'; - -export interface ToggleBlameCommandArgs { - sha?: string; -} - -export class ToggleBlameCommand extends EditorCommand { - - constructor(private annotationController: BlameAnnotationController) { - super(Commands.ToggleBlame); - } - - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleBlameCommandArgs = {}): Promise { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - - try { - return this.annotationController.toggleBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line); - } - catch (ex) { - Logger.error(ex, 'ToggleBlameCommand'); - return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`); - } - } -} \ No newline at end of file diff --git a/src/commands/toggleFileBlame.ts b/src/commands/toggleFileBlame.ts new file mode 100644 index 0000000..901d1d2 --- /dev/null +++ b/src/commands/toggleFileBlame.ts @@ -0,0 +1,35 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; +import { AnnotationController } from '../annotations/annotationController'; +import { Commands, EditorCommand } from './common'; +import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { Logger } from '../logger'; + +export interface ToggleFileBlameCommandArgs { + sha?: string; + type?: FileAnnotationType; +} + +export class ToggleFileBlameCommand extends EditorCommand { + + constructor(private annotationController: AnnotationController) { + super(Commands.ToggleFileBlame); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; + + try { + if (args.type === undefined) { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + args.type = cfg.blame.file.annotationType; + } + + return this.annotationController.toggleAnnotations(editor, args.type, args.sha !== undefined ? args.sha : editor.selection.active.line); + } + catch (ex) { + Logger.error(ex, 'ToggleFileBlameCommand'); + return window.showErrorMessage(`Unable to toggle file blame annotations. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/toggleLineBlame.ts b/src/commands/toggleLineBlame.ts new file mode 100644 index 0000000..3742ddd --- /dev/null +++ b/src/commands/toggleLineBlame.ts @@ -0,0 +1,34 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; +import { CurrentLineController } from '../currentLineController'; +import { Commands, EditorCommand } from './common'; +import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration'; +import { Logger } from '../logger'; + +export interface ToggleLineBlameCommandArgs { + type?: LineAnnotationType; +} + +export class ToggleLineBlameCommand extends EditorCommand { + + constructor(private currentLineController: CurrentLineController) { + super(Commands.ToggleLineBlame); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; + + try { + if (args.type === undefined) { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + args.type = cfg.blame.line.annotationType; + } + + return this.currentLineController.toggleAnnotations(editor, args.type); + } + catch (ex) { + Logger.error(ex, 'ToggleLineBlameCommand'); + return window.showErrorMessage(`Unable to toggle line blame annotations. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index bd686d2..5975bd5 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -2,31 +2,18 @@ import { Commands } from './commands'; import { OutputLevel } from './logger'; -export type BlameAnnotationStyle = 'compact' | 'expanded' | 'trailing'; -export const BlameAnnotationStyle = { - Compact: 'compact' as BlameAnnotationStyle, - Expanded: 'expanded' as BlameAnnotationStyle, - Trailing: 'trailing' as BlameAnnotationStyle -}; +export { ExtensionKey } from './constants'; -export interface IBlameConfig { - annotation: { - style: BlameAnnotationStyle; - highlight: 'none' | 'gutter' | 'line' | 'both'; - sha: boolean; - author: boolean; - date: 'off' | 'relative' | 'absolute'; - dateFormat: string; - message: boolean; - activeLine: 'off' | 'inline' | 'hover' | 'both'; - activeLineDarkColor: string; - activeLineLightColor: string; - }; -} +export type BlameLineHighlightLocations = 'gutter' | 'line' | 'overviewRuler'; +export const BlameLineHighlightLocations = { + Gutter: 'gutter' as BlameLineHighlightLocations, + Line: 'line' as BlameLineHighlightLocations, + OverviewRuler: 'overviewRuler' as BlameLineHighlightLocations +}; -export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory'; +export type CodeLensCommand = 'gitlens.toggleFileBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory'; export const CodeLensCommand = { - BlameAnnotate: Commands.ToggleBlame as CodeLensCommand, + BlameAnnotate: Commands.ToggleFileBlame as CodeLensCommand, ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand, ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand, @@ -36,46 +23,29 @@ export const CodeLensCommand = { ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as CodeLensCommand }; -export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom' | 'none'; -export const CodeLensLocation = { - All: 'all' as CodeLensLocation, - DocumentAndContainers: 'document+containers' as CodeLensLocation, - Document: 'document' as CodeLensLocation, - Custom: 'custom' as CodeLensLocation, - None: 'none' as CodeLensLocation +export type CodeLensLocations = 'document' | 'containers' | 'blocks' | 'custom'; +export const CodeLensLocations = { + Document: 'document' as CodeLensLocations, + Containers: 'containers' as CodeLensLocations, + Blocks: 'blocks' as CodeLensLocations, + Custom: 'custom' as CodeLensLocations }; -export type CodeLensVisibility = 'auto' | 'ondemand' | 'off'; -export const CodeLensVisibility = { - Auto: 'auto' as CodeLensVisibility, - OnDemand: 'ondemand' as CodeLensVisibility, - Off: 'off' as CodeLensVisibility +export type FileAnnotationType = 'gutter' | 'hover'; +export const FileAnnotationType = { + Gutter: 'gutter' as FileAnnotationType, + Hover: 'hover' as FileAnnotationType }; -export interface ICodeLensConfig { - enabled: boolean; - command: CodeLensCommand; -} - -export interface ICodeLensLanguageLocation { - language: string | undefined; - location: CodeLensLocation; - customSymbols?: string[]; -} - -export interface ICodeLensesConfig { - debug: boolean; - visibility: CodeLensVisibility; - location: CodeLensLocation; - locationCustomSymbols: string[]; - languageLocations: ICodeLensLanguageLocation[]; - recentChange: ICodeLensConfig; - authors: ICodeLensConfig; -} +export type LineAnnotationType = 'trailing' | 'hover'; +export const LineAnnotationType = { + Trailing: 'trailing' as LineAnnotationType, + Hover: 'hover' as LineAnnotationType +}; -export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory'; +export type StatusBarCommand = 'gitlens.toggleFileBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory'; export const StatusBarCommand = { - BlameAnnotate: Commands.ToggleBlame as StatusBarCommand, + BlameAnnotate: Commands.ToggleFileBlame as StatusBarCommand, ShowBlameHistory: Commands.ShowBlameHistory as StatusBarCommand, ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand, @@ -87,26 +57,44 @@ export const StatusBarCommand = { ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as StatusBarCommand }; -export interface IStatusBarConfig { - enabled: boolean; - command: StatusBarCommand; - date: 'off' | 'relative' | 'absolute'; - dateFormat: string; - alignment: 'left' | 'right'; -} - export interface IAdvancedConfig { caching: { enabled: boolean; - statusBar: { - maxLines: number; - } + maxLines: number; }; git: string; gitignore: { enabled: boolean; }; maxQuickHistory: number; + menus: { + explorerContext: { + fileDiff: boolean; + history: boolean; + remote: boolean; + }; + editorContext: { + blame: boolean; + copy: boolean; + details: boolean; + fileDiff: boolean; + history: boolean; + lineDiff: boolean; + remote: boolean; + }; + editorTitle: { + blame: boolean; + fileDiff: boolean; + history: boolean; + status: boolean; + }; + editorTitleContext: { + blame: boolean; + fileDiff: boolean; + history: boolean; + remote: boolean; + }; + }; quickPick: { closeOnFocusOut: boolean; }; @@ -115,12 +103,192 @@ export interface IAdvancedConfig { }; } +export interface ICodeLensLanguageLocation { + language: string | undefined; + locations: CodeLensLocations[]; + customSymbols?: string[]; +} + +export interface IThemeConfig { + annotations: { + file: { + gutter: { + separateLines: boolean; + dark: { + backgroundColor: string | null; + foregroundColor: string; + uncommittedForegroundColor: string | null; + }; + light: { + backgroundColor: string | null; + foregroundColor: string; + uncommittedForegroundColor: string | null; + }; + }; + + hover: { + separateLines: boolean; + }; + }; + + line: { + trailing: { + dark: { + backgroundColor: string | null; + foregroundColor: string; + }; + light: { + backgroundColor: string | null; + foregroundColor: string; + }; + }; + }; + }; + + lineHighlight: { + dark: { + backgroundColor: string; + overviewRulerColor: string; + }; + light: { + backgroundColor: string; + overviewRulerColor: string; + }; + }; +} + +export const themeDefaults: IThemeConfig = { + annotations: { + file: { + gutter: { + separateLines: true, + dark: { + backgroundColor: null, + foregroundColor: 'rgb(190, 190, 190)', + uncommittedForegroundColor: null + }, + light: { + backgroundColor: null, + foregroundColor: 'rgb(116, 116, 116)', + uncommittedForegroundColor: null + } + }, + hover: { + separateLines: false + } + }, + line: { + trailing: { + dark: { + backgroundColor: null, + foregroundColor: 'rgba(153, 153, 153, 0.35)' + }, + light: { + backgroundColor: null, + foregroundColor: 'rgba(153, 153, 153, 0.35)' + } + } + } + }, + lineHighlight: { + dark: { + backgroundColor: 'rgba(0, 188, 242, 0.2)', + overviewRulerColor: 'rgba(0, 188, 242, 0.6)' + }, + light: { + backgroundColor: 'rgba(0, 188, 242, 0.2)', + overviewRulerColor: 'rgba(0, 188, 242, 0.6)' + } + } +}; + export interface IConfig { + annotations: { + file: { + gutter: { + format: string; + dateFormat: string; + compact: boolean; + heatmap: { + enabled: boolean; + location: 'left' | 'right'; + }; + hover: { + details: boolean; + wholeLine: boolean; + }; + }; + + hover: { + heatmap: { + enabled: boolean; + }; + wholeLine: boolean; + }; + }; + + line: { + hover: { + details: boolean; + changes: boolean; + }; + + trailing: { + format: string; + dateFormat: string; + hover: { + changes: boolean; + details: boolean; + wholeLine: boolean; + }; + }; + }; + }; + + blame: { + file: { + annotationType: FileAnnotationType; + lineHighlight: { + enabled: boolean; + locations: BlameLineHighlightLocations[]; + }; + }; + + line: { + enabled: boolean; + annotationType: LineAnnotationType; + }; + }; + + codeLens: { + enabled: boolean; + recentChange: { + enabled: boolean; + command: CodeLensCommand; + }; + authors: { + enabled: boolean; + command: CodeLensCommand; + }; + locations: CodeLensLocations[]; + customLocationSymbols: string[]; + perLanguageLocations: ICodeLensLanguageLocation[]; + debug: boolean; + }; + + statusBar: { + enabled: boolean; + alignment: 'left' | 'right'; + command: StatusBarCommand; + format: string; + dateFormat: string; + }; + + theme: IThemeConfig; + debug: boolean; + insiders: boolean; outputLevel: OutputLevel; - blame: IBlameConfig; - codeLens: ICodeLensesConfig; - statusBar: IStatusBarConfig; + advanced: IAdvancedConfig; - insiders: boolean; } \ No newline at end of file diff --git a/src/currentLineController.ts b/src/currentLineController.ts new file mode 100644 index 0000000..f19cd31 --- /dev/null +++ b/src/currentLineController.ts @@ -0,0 +1,437 @@ +'use strict'; +import { Functions, Objects } from './system'; +import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { AnnotationController } from './annotations/annotationController'; +import { Annotations, endOfLineIndex } from './annotations/annotations'; +import { Commands } from './commands'; +import { TextEditorComparer } from './comparers'; +import { FileAnnotationType, IConfig, LineAnnotationType, StatusBarCommand } from './configuration'; +import { DocumentSchemes, ExtensionKey } from './constants'; +import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; + +const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 4em' + } +} as DecorationRenderOptions); + +export class CurrentLineController extends Disposable { + + private _activeEditorLineDisposable: Disposable | undefined; + private _blameable: boolean; + private _config: IConfig; + private _currentLine: number = -1; + private _disposable: Disposable; + private _editor: TextEditor | undefined; + private _statusBarItem: StatusBarItem | undefined; + private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise; + private _uri: GitUri; + + constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: AnnotationController) { + super(() => this.dispose()); + + this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250); + + this._onConfigurationChanged(); + + const subscriptions: Disposable[] = []; + + subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); + subscriptions.push(git.onDidChangeGitCache(this._onGitCacheChanged, this)); + subscriptions.push(annotationController.onDidToggleAnnotations(this._onAnnotationsToggled, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + this._editor && this._editor.setDecorations(annotationDecoration, []); + + this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose(); + this._statusBarItem && this._statusBarItem.dispose(); + this._disposable && this._disposable.dispose(); + } + + private _onConfigurationChanged() { + const cfg = workspace.getConfiguration().get(ExtensionKey)!; + + let changed = false; + + if (!Objects.areEquivalent(cfg.blame.line, this._config && this._config.blame.line) || + !Objects.areEquivalent(cfg.annotations.line.trailing, this._config && this._config.annotations.line.trailing) || + !Objects.areEquivalent(cfg.annotations.line.hover, this._config && this._config.annotations.line.hover) || + !Objects.areEquivalent(cfg.theme.annotations.line.trailing, this._config && this._config.theme.annotations.line.trailing)) { + changed = true; + if (this._editor) { + this._editor.setDecorations(annotationDecoration, []); + } + } + + if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { + changed = true; + if (cfg.statusBar.enabled) { + const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; + if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) { + this._statusBarItem.dispose(); + this._statusBarItem = undefined; + } + + this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); + this._statusBarItem.command = cfg.statusBar.command; + } + else if (!cfg.statusBar.enabled && this._statusBarItem) { + this._statusBarItem.dispose(); + this._statusBarItem = undefined; + } + } + + this._config = cfg; + + if (!changed) return; + + const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled; + if (trackCurrentLine && !this._activeEditorLineDisposable) { + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); + subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); + subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this)); + + this._activeEditorLineDisposable = Disposable.from(...subscriptions); + } + else if (!trackCurrentLine && this._activeEditorLineDisposable) { + this._activeEditorLineDisposable.dispose(); + this._activeEditorLineDisposable = undefined; + } + + this._onActiveTextEditorChanged(window.activeTextEditor); + } + + private isEditorBlameable(editor: TextEditor | undefined): boolean { + if (editor === undefined || editor.document === undefined) return false; + + if (!this.git.isTrackable(editor.document.uri)) return false; + if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false; + + return this.git.isEditorBlameable(editor); + } + + private async _onActiveTextEditorChanged(editor: TextEditor | undefined) { + this._currentLine = -1; + + const previousEditor = this._editor; + previousEditor && previousEditor.setDecorations(annotationDecoration, []); + + if (editor === undefined || !this.isEditorBlameable(editor)) { + this.clear(editor); + + this._editor = undefined; + + return; + } + + this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; + this._editor = editor; + this._uri = await GitUri.fromUri(editor.document.uri, this.git); + + const maxLines = this._config.advanced.caching.maxLines; + // If caching is on and the file is small enough -- kick off a blame for the whole file + if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { + this.git.getBlameForFile(this._uri); + } + + this._updateBlameDebounced(editor.selection.active.line, editor); + } + + private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + this._blameable = e.blameable; + if (!e.blameable || !this._editor) { + this.clear(e.editor); + return; + } + + // Make sure this is for the editor we are tracking + if (!TextEditorComparer.equals(this._editor, e.editor)) return; + + this._updateBlameDebounced(this._editor.selection.active.line, this._editor); + } + + private _onAnnotationsToggled() { + this._onActiveTextEditorChanged(window.activeTextEditor); + } + + private _onGitCacheChanged() { + this._onActiveTextEditorChanged(window.activeTextEditor); + } + + private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { + // Make sure this is for the editor we are tracking + if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; + + const line = e.selections[0].active.line; + if (line === this._currentLine) return; + this._currentLine = line; + + if (!this._uri && e.textEditor) { + this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git); + } + + this._updateBlameDebounced(line, e.textEditor); + } + + private async _updateBlame(line: number, editor: TextEditor) { + line = line - this._uri.offset; + + let commit: GitCommit | undefined = undefined; + let commitLine: IGitCommitLine | undefined = undefined; + // Since blame information isn't valid when there are unsaved changes -- don't show any status + if (this._blameable && line >= 0) { + const blameLine = await this.git.getBlameForLine(this._uri, line); + commitLine = blameLine === undefined ? undefined : blameLine.line; + commit = blameLine === undefined ? undefined : blameLine.commit; + } + + if (commit !== undefined && commitLine !== undefined) { + this.show(commit, commitLine, editor); + } + else { + this.clear(editor); + } + } + + async clear(editor: TextEditor | undefined, previousEditor?: TextEditor) { + this._clearAnnotations(editor, previousEditor); + this._statusBarItem && this._statusBarItem.hide(); + } + + private async _clearAnnotations(editor: TextEditor | undefined, previousEditor?: TextEditor) { + editor && editor.setDecorations(annotationDecoration, []); + // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay + if (editor !== undefined) { + await Functions.wait(1); + editor.setDecorations(annotationDecoration, []); + } + } + + async show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { + // I have no idea why I need this protection -- but it happens + if (editor.document === undefined) return; + + this._updateStatusBar(commit); + await this._updateAnnotations(commit, blameLine, editor); + } + + async showAnnotations(editor: TextEditor, type: LineAnnotationType) { + if (editor === undefined) return; + + const cfg = this._config.blame.line; + if (!cfg.enabled || cfg.annotationType !== type) { + cfg.enabled = true; + cfg.annotationType = type; + + await this._clearAnnotations(editor); + await this._updateBlame(editor.selection.active.line, editor); + } + } + + async toggleAnnotations(editor: TextEditor, type: LineAnnotationType) { + if (editor === undefined) return; + + const cfg = this._config.blame.line; + cfg.enabled = !cfg.enabled; + cfg.annotationType = type; + + await this._clearAnnotations(editor); + await this._updateBlame(editor.selection.active.line, editor); + } + + private async _updateAnnotations(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { + const cfg = this._config.blame.line; + if (!cfg.enabled) return; + + const line = blameLine.line + this._uri.offset; + + const decorationOptions: DecorationOptions[] = []; + + let showChanges = false; + let showChangesStartIndex = 0; + let showChangesInStartingWhitespace = false; + + let showDetails = false; + let showDetailsStartIndex = 0; + let showDetailsInStartingWhitespace = false; + + switch (cfg.annotationType) { + case LineAnnotationType.Trailing: { + const cfgAnnotations = this._config.annotations.line.trailing; + + showChanges = cfgAnnotations.hover.changes; + showDetails = cfgAnnotations.hover.details; + + if (cfgAnnotations.hover.wholeLine) { + showChangesStartIndex = 0; + showChangesInStartingWhitespace = false; + + showDetailsStartIndex = 0; + showDetailsInStartingWhitespace = false; + } + else { + showChangesStartIndex = endOfLineIndex; + showChangesInStartingWhitespace = true; + + showDetailsStartIndex = endOfLineIndex; + showDetailsInStartingWhitespace = true; + } + + const decoration = Annotations.trailing(commit, cfgAnnotations.format, cfgAnnotations.dateFormat, this._config.theme); + decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex)); + decorationOptions.push(decoration); + + break; + } + case LineAnnotationType.Hover: { + const cfgAnnotations = this._config.annotations.line.hover; + + showChanges = cfgAnnotations.changes; + showChangesStartIndex = 0; + showChangesInStartingWhitespace = false; + + showDetails = cfgAnnotations.details; + showDetailsStartIndex = 0; + showDetailsInStartingWhitespace = false; + + break; + } + } + + if (showDetails || showChanges) { + const annotationType = this.annotationController.getAnnotationType(editor); + + const firstNonWhitespace = editor.document.lineAt(line).firstNonWhitespaceCharacterIndex; + + switch (annotationType) { + case FileAnnotationType.Gutter: { + const cfgHover = this._config.annotations.file.gutter.hover; + if (cfgHover.details) { + showDetailsInStartingWhitespace = false; + if (cfgHover.wholeLine) { + // Avoid double annotations if we are showing the whole-file hover blame annotations + showDetails = false; + } + else { + if (showDetailsStartIndex === 0) { + showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace; + } + if (showChangesStartIndex === 0) { + showChangesInStartingWhitespace = true; + showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace; + } + } + } + + break; + } + case FileAnnotationType.Hover: { + const cfgHover = this._config.annotations.file.hover; + showDetailsInStartingWhitespace = false; + if (cfgHover.wholeLine) { + // Avoid double annotations if we are showing the whole-file hover blame annotations + showDetails = false; + showChangesStartIndex = 0; + } + else { + if (showDetailsStartIndex === 0) { + showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace; + } + if (showChangesStartIndex === 0) { + showChangesInStartingWhitespace = true; + showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace; + } + } + + break; + } + } + + if (showDetails) { + // Get the full commit message -- since blame only returns the summary + let logCommit: GitCommit | undefined = undefined; + if (!commit.isUncommitted) { + logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha); + } + + // I have no idea why I need this protection -- but it happens + if (editor.document === undefined) return; + + const decoration = Annotations.detailsHover(logCommit || commit); + decoration.range = editor.document.validateRange(new Range(line, showDetailsStartIndex, line, endOfLineIndex)); + decorationOptions.push(decoration); + + if (showDetailsInStartingWhitespace && showDetailsStartIndex !== 0) { + decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace)); + } + } + + if (showChanges) { + const decoration = await Annotations.changesHover(commit, line, this._uri, this.git); + + // I have no idea why I need this protection -- but it happens + if (editor.document === undefined) return; + + decoration.range = editor.document.validateRange(new Range(line, showChangesStartIndex, line, endOfLineIndex)); + decorationOptions.push(decoration); + + if (showChangesInStartingWhitespace && showChangesStartIndex !== 0) { + decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace)); + } + } + } + + if (decorationOptions.length) { + editor.setDecorations(annotationDecoration, decorationOptions); + } + } + + private _updateStatusBar(commit: GitCommit) { + const cfg = this._config.statusBar; + if (!cfg.enabled || this._statusBarItem === undefined) return; + + this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, cfg.dateFormat)}`; + + switch (cfg.command) { + case StatusBarCommand.BlameAnnotate: + this._statusBarItem.tooltip = 'Toggle Blame Annotations'; + break; + case StatusBarCommand.ShowBlameHistory: + this._statusBarItem.tooltip = 'Open Blame History Explorer'; + break; + case StatusBarCommand.ShowFileHistory: + this._statusBarItem.tooltip = 'Open File History Explorer'; + break; + case StatusBarCommand.DiffWithPrevious: + this._statusBarItem.command = Commands.DiffLineWithPrevious; + this._statusBarItem.tooltip = 'Compare Line Commit with Previous'; + break; + case StatusBarCommand.DiffWithWorking: + this._statusBarItem.command = Commands.DiffLineWithWorking; + this._statusBarItem.tooltip = 'Compare Line Commit with Working Tree'; + break; + case StatusBarCommand.ToggleCodeLens: + this._statusBarItem.tooltip = 'Toggle Git CodeLens'; + break; + case StatusBarCommand.ShowQuickCommitDetails: + this._statusBarItem.tooltip = 'Show Commit Details'; + break; + case StatusBarCommand.ShowQuickCommitFileDetails: + this._statusBarItem.tooltip = 'Show Line Commit Details'; + break; + case StatusBarCommand.ShowQuickFileHistory: + this._statusBarItem.tooltip = 'Show File History'; + break; + case StatusBarCommand.ShowQuickCurrentBranchHistory: + this._statusBarItem.tooltip = 'Show Branch History'; + break; + } + + this._statusBarItem.show(); + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 9380c40..86fcfb9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,14 +1,13 @@ 'use strict'; import { Objects } from './system'; import { commands, ExtensionContext, extensions, languages, Uri, window, workspace } from 'vscode'; -import { BlameActiveLineController } from './blameActiveLineController'; -import { BlameAnnotationController } from './blameAnnotationController'; +import { AnnotationController } from './annotations/annotationController'; import { CommandContext, setCommandContext } from './commands'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; -import { ShowBlameCommand, ToggleBlameCommand } from './commands'; +import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleLineBlameCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; import { ShowLastQuickPickCommand } from './commands'; import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands'; @@ -19,6 +18,7 @@ import { ToggleCodeLensCommand } from './commands'; import { Keyboard } from './commands'; import { IConfig } from './configuration'; import { ApplicationInsightsKey, BuiltInCommands, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants'; +import { CurrentLineController } from './currentLineController'; import { GitContentProvider } from './gitContentProvider'; import { GitContextTracker, GitService } from './gitService'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; @@ -74,11 +74,11 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git))); - const annotationController = new BlameAnnotationController(context, git, gitContextTracker); + const annotationController = new AnnotationController(context, git, gitContextTracker); context.subscriptions.push(annotationController); - const activeLineController = new BlameActiveLineController(context, git, gitContextTracker, annotationController); - context.subscriptions.push(activeLineController); + const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController); + context.subscriptions.push(currentLineController); context.subscriptions.push(new Keyboard()); @@ -98,8 +98,10 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new OpenFileInRemoteCommand(git)); context.subscriptions.push(new OpenInRemoteCommand()); context.subscriptions.push(new OpenRepoInRemoteCommand(git)); - context.subscriptions.push(new ShowBlameCommand(annotationController)); - context.subscriptions.push(new ToggleBlameCommand(annotationController)); + context.subscriptions.push(new ShowFileBlameCommand(annotationController)); + context.subscriptions.push(new ShowLineBlameCommand(currentLineController)); + context.subscriptions.push(new ToggleFileBlameCommand(annotationController)); + context.subscriptions.push(new ToggleLineBlameCommand(currentLineController)); context.subscriptions.push(new ShowBlameHistoryCommand(git)); context.subscriptions.push(new ShowFileHistoryCommand(git)); context.subscriptions.push(new ShowLastQuickPickCommand()); diff --git a/src/git/formatters/commit.ts b/src/git/formatters/commit.ts new file mode 100644 index 0000000..aef578a --- /dev/null +++ b/src/git/formatters/commit.ts @@ -0,0 +1,160 @@ +'use strict'; +import { Strings } from '../../system'; +import { GitCommit } from '../models/commit'; +import { IGitDiffLine } from '../models/diff'; +import * as moment from 'moment'; + +export interface ICommitFormatOptions { + dateFormat?: string | null; + tokenOptions?: { + ago?: Strings.ITokenOptions; + author?: Strings.ITokenOptions; + authorAgo?: Strings.ITokenOptions; + date?: Strings.ITokenOptions; + message?: Strings.ITokenOptions; + }; +} + +export class CommitFormatter { + + private _options: ICommitFormatOptions; + + constructor(private commit: GitCommit, options?: ICommitFormatOptions) { + options = options || {}; + if (options.tokenOptions == null) { + options.tokenOptions = {}; + } + + if (options.dateFormat == null) { + options.dateFormat = 'MMMM Do, YYYY h:MMa'; + } + + this._options = options; + } + + get ago() { + const ago = moment(this.commit.date).fromNow(); + return this._padOrTruncate(ago, this._options.tokenOptions!.ago); + } + + get author() { + const author = this.commit.author; + return this._padOrTruncate(author, this._options.tokenOptions!.author); + } + + get authorAgo() { + const authorAgo = `${this.commit.author}, ${moment(this.commit.date).fromNow()}`; + return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo); + } + + get date() { + const date = moment(this.commit.date).format(this._options.dateFormat!); + return this._padOrTruncate(date, this._options.tokenOptions!.date); + } + + get id() { + return this.commit.shortSha; + } + + get message() { + const message = this.commit.isUncommitted ? 'Uncommitted change' : this.commit.message; + return this._padOrTruncate(message, this._options.tokenOptions!.message); + } + + get sha() { + return this.id; + } + + private collapsableWhitespace: number = 0; + + private _padOrTruncate(s: string, options: Strings.ITokenOptions | undefined) { + // NOTE: the collapsable whitespace logic relies on the javascript template evaluation to be left to right + if (options === undefined) { + options = { + truncateTo: undefined, + padDirection: 'left', + collapseWhitespace: false + }; + } + + let max = options.truncateTo; + + if (max === undefined) { + if (this.collapsableWhitespace === 0) return s; + + // If we have left over whitespace make sure it gets re-added + const diff = this.collapsableWhitespace - s.length; + this.collapsableWhitespace = 0; + + if (diff <= 0) return s; + if (options.truncateTo === undefined) return s; + return Strings.padLeft(s, diff); + } + + max += this.collapsableWhitespace; + this.collapsableWhitespace = 0; + + const diff = max - s.length; + if (diff > 0) { + if (options.collapseWhitespace) { + this.collapsableWhitespace = diff; + } + + if (options.padDirection === 'left') return Strings.padLeft(s, max); + + if (options.collapseWhitespace) { + max -= diff; + } + return Strings.padRight(s, max); + } + + if (diff < 0) return Strings.truncate(s, max); + + return s; + } + + static fromTemplate(template: string, commit: GitCommit, dateFormat: string | null): string; + static fromTemplate(template: string, commit: GitCommit, options?: ICommitFormatOptions): string; + static fromTemplate(template: string, commit: GitCommit, dateFormatOrOptions?: string | null | ICommitFormatOptions): string; + static fromTemplate(template: string, commit: GitCommit, dateFormatOrOptions?: string | null | ICommitFormatOptions): string { + let options: ICommitFormatOptions | undefined = undefined; + if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') { + const tokenOptions = Strings.getTokensFromTemplate(template) + .reduce((map, token) => { + map[token.key] = token.options; + return map; + }, {} as { [token: string]: ICommitFormatOptions }); + + options = { + dateFormat: dateFormatOrOptions, + tokenOptions: tokenOptions + }; + } + else { + options = dateFormatOrOptions; + } + + return Strings.interpolateLazy(template, new CommitFormatter(commit, options)); + } + + static toHoverAnnotation(commit: GitCommit, dateFormat: string = 'MMMM Do, YYYY h:MMa'): string | string[] { + const message = commit.isUncommitted ? '' : `\n\n> ${commit.message.replace(/\n/g, '\n>\n> ')}`; + return `\`${commit.shortSha}\`   __${commit.author}__, ${moment(commit.date).fromNow()}   _(${moment(commit.date).format(dateFormat)})_${message}`; + } + + static toHoverDiff(commit: GitCommit, previous: IGitDiffLine | undefined, current: IGitDiffLine | undefined): string | undefined { + if (previous === undefined && current === undefined) return undefined; + + const codeDiff = this._getCodeDiff(previous, current); + return commit.isUncommitted + ? `\`Changes\`   \u2014   _uncommitted_\n${codeDiff}` + : `\`Changes\`   \u2014   \`${commit.previousShortSha}\` \u2194 \`${commit.shortSha}\`\n${codeDiff}`; + } + + private static _getCodeDiff(previous: IGitDiffLine | undefined, current: IGitDiffLine | undefined): string { + return `\`\`\` +- ${previous === undefined ? '' : previous.line.trim()} ++ ${current === undefined ? '' : current.line.trim()} +\`\`\``; + } +} \ No newline at end of file diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts index a52cf5d..51c1d3e 100644 --- a/src/git/models/diff.ts +++ b/src/git/models/diff.ts @@ -1,11 +1,16 @@ 'use strict'; +export interface IGitDiffLine { + line: string; + state: 'added' | 'removed' | 'unchanged'; +} + export interface IGitDiffChunk { - current: (string | undefined)[]; + current: (IGitDiffLine | undefined)[]; currentStart: number; currentEnd: number; - previous: (string | undefined)[]; + previous: (IGitDiffLine | undefined)[]; previousStart: number; previousEnd: number; diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 1c20438..125b939 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -60,7 +60,7 @@ export class GitBlameParser { switch (lineParts[0]) { case 'author': entry.author = Git.isUncommitted(entry.sha) - ? 'Uncommitted' + ? 'You' : lineParts.slice(1).join(' ').trim(); break; diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index 0d96cb7..569aac6 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -1,5 +1,5 @@ 'use strict'; -import { IGitDiff, IGitDiffChunk } from './../git'; +import { IGitDiff, IGitDiffChunk, IGitDiffLine } from './../git'; const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; @@ -21,23 +21,29 @@ export class GitDiffParser { const chunk = match[5]; const lines = chunk.split('\n').slice(1); - const current = []; - const previous = []; + const current: (IGitDiffLine | undefined)[] = []; + const previous: (IGitDiffLine | undefined)[] = []; for (const l of lines) { switch (l[0]) { case '+': - current.push(` ${l.substring(1)}`); + current.push({ + line: ` ${l.substring(1)}`, + state: 'added' + }); previous.push(undefined); break; case '-': current.push(undefined); - previous.push(` ${l.substring(1)}`); + previous.push({ + line: ` ${l.substring(1)}`, + state: 'removed' + }); break; default: - current.push(l); - previous.push(l); + current.push({ line: l, state: 'unchanged' }); + previous.push({ line: l, state: 'unchanged' }); break; } } diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 3f69f75..3bc13a0 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -61,7 +61,7 @@ export class GitLogParser { switch (lineParts[0]) { case 'author': entry.author = Git.isUncommitted(entry.sha) - ? 'Uncommitted' + ? 'You' : lineParts.slice(1).join(' ').trim(); break; diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index d56d253..f67a3a7 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -3,7 +3,7 @@ import { Functions, Iterables, Strings } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands'; import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants'; -import { CodeLensCommand, CodeLensLocation, ICodeLensLanguageLocation, IConfig } from './configuration'; +import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration'; import { GitCommit, GitService, GitUri, IGitBlame, IGitBlameLines } from './gitService'; import { Logger } from './logger'; import * as moment from 'moment'; @@ -56,24 +56,22 @@ export class GitCodeLensProvider implements CodeLensProvider { async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { this._documentIsDirty = document.isDirty; - let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId); + let languageLocations = this._config.codeLens.perLanguageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId); if (languageLocations == null) { languageLocations = { language: undefined, - location: this._config.codeLens.location, - customSymbols: this._config.codeLens.locationCustomSymbols + locations: this._config.codeLens.locations, + customSymbols: this._config.codeLens.customLocationSymbols } as ICodeLensLanguageLocation; } const lenses: CodeLens[] = []; - if (languageLocations.location === CodeLensLocation.None) return lenses; - const gitUri = await GitUri.fromUri(document.uri, this.git); const blamePromise = this.git.getBlameForFile(gitUri); let blame: IGitBlame | undefined; - if (languageLocations.location === CodeLensLocation.Document) { + if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) { blame = await blamePromise; if (blame === undefined || !blame.lines.length) return lenses; } @@ -91,7 +89,8 @@ export class GitCodeLensProvider implements CodeLensProvider { symbols.forEach(sym => this._provideCodeLens(gitUri, document, sym, languageLocations!, blame!, lenses)); } - if (languageLocations.location !== CodeLensLocation.Custom || (languageLocations.customSymbols || []).find(_ => _.toLowerCase() === 'file')) { + if (languageLocations.locations.includes(CodeLensLocations.Document) || + (languageLocations.locations.includes(CodeLensLocations.Custom) && (languageLocations.customSymbols || []).find(_ => _.toLowerCase() === 'file'))) { // Check if we have a lens for the whole document -- if not add one if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); @@ -117,55 +116,61 @@ export class GitCodeLensProvider implements CodeLensProvider { private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined { let valid = false; let range: Range | undefined; - switch (languageLocation.location) { - case CodeLensLocation.All: - case CodeLensLocation.DocumentAndContainers: - switch (symbol.kind) { - case SymbolKind.File: - valid = true; - // Adjust the range to be the whole file - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); - break; - case SymbolKind.Package: - case SymbolKind.Module: - // Adjust the range to be the whole file - if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) { - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); - } - valid = true; - break; - case SymbolKind.Namespace: - case SymbolKind.Class: - case SymbolKind.Interface: - valid = true; - break; - case SymbolKind.Constructor: - case SymbolKind.Method: - case SymbolKind.Function: - case SymbolKind.Property: - case SymbolKind.Enum: - valid = languageLocation.location === CodeLensLocation.All; - break; + + switch (symbol.kind) { + case SymbolKind.File: + if (languageLocation.locations.includes(CodeLensLocations.Containers)) { + valid = true; + } + else if (languageLocation.locations.includes(CodeLensLocations.Custom)) { + valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase()); + } + + if (valid) { + // Adjust the range to be for the whole file + range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); } break; - case CodeLensLocation.Custom: - valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase()); + + case SymbolKind.Package: + if (languageLocation.locations.includes(CodeLensLocations.Containers)) { + valid = true; + } + else if (languageLocation.locations.includes(CodeLensLocations.Custom)) { + valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase()); + } + if (valid) { - switch (symbol.kind) { - case SymbolKind.File: - // Adjust the range to be the whole file - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); - break; - case SymbolKind.Package: - case SymbolKind.Module: - // Adjust the range to be the whole file - if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) { - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); - } - break; + // Adjust the range to be for the whole file + if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) { + range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); } } break; + + case SymbolKind.Class: + case SymbolKind.Interface: + case SymbolKind.Module: + case SymbolKind.Namespace: + case SymbolKind.Struct: + if (languageLocation.locations.includes(CodeLensLocations.Containers)) { + valid = true; + } + break; + + case SymbolKind.Constructor: + case SymbolKind.Enum: + case SymbolKind.Function: + case SymbolKind.Method: + case SymbolKind.Property: + if (languageLocation.locations.includes(CodeLensLocations.Blocks)) { + valid = true; + } + break; + } + + if (!valid && languageLocation.locations.includes(CodeLensLocations.Custom)) { + valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase()); } return valid ? range || symbol.location.range : undefined; @@ -302,7 +307,7 @@ export class GitCodeLensProvider implements CodeLensProvider { _applyBlameAnnotateCommand(title: string, lens: T, blame: IGitBlameLines): T { lens.command = { title: title, - command: Commands.ToggleBlame, + command: Commands.ToggleFileBlame, arguments: [Uri.file(lens.uri.fsPath)] }; return lens; diff --git a/src/gitService.ts b/src/gitService.ts index 68989e3..7962968 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -2,9 +2,9 @@ import { Iterables, Objects } from './system'; import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode'; import { CommandContext, setCommandContext } from './commands'; -import { CodeLensVisibility, IConfig } from './configuration'; +import { IConfig } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; -import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitLog, IGitStash, IGitStatus, setDefaultEncoding } from './git/git'; +import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitDiffLine, IGitLog, IGitStash, IGitStatus, setDefaultEncoding } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { GitCodeLensProvider } from './gitCodeLensProvider'; import { Logger } from './logger'; @@ -15,6 +15,7 @@ import * as path from 'path'; export { GitUri, IGitCommitInfo }; export * from './git/models/models'; +export * from './git/formatters/commit'; export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider'; export * from './git/gitContextTracker'; @@ -139,7 +140,7 @@ export class GitService extends Disposable { if (codeLensChanged) { Logger.log('CodeLens config changed; resetting CodeLens provider'); - if (cfg.codeLens.visibility === CodeLensVisibility.Auto && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { + if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { if (this._codeLensProvider) { this._codeLensProvider.reset(); } @@ -154,7 +155,7 @@ export class GitService extends Disposable { this._codeLensProvider = undefined; } - setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.visibility !== CodeLensVisibility.Off && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)); + setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled); } if (advancedChanged) { @@ -644,13 +645,13 @@ export class GitService extends Disposable { } } - async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<[string | undefined, string | undefined] | undefined> { + async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<[IGitDiffLine | undefined, IGitDiffLine | undefined]> { try { const diff = await this.getDiffForFile(uri, sha1, sha2); - if (diff === undefined) return undefined; + if (diff === undefined) return [undefined, undefined]; const chunk = diff.chunks.find(_ => _.currentStart <= line && _.currentEnd >= line); - if (chunk === undefined) return undefined; + if (chunk === undefined) return [undefined, undefined]; // Search for the line (skipping deleted lines -- since they don't currently exist in the editor) // Keep track of the deleted lines for the original version @@ -675,7 +676,7 @@ export class GitService extends Disposable { ]; } catch (ex) { - return undefined; + return [undefined, undefined]; } } @@ -1008,8 +1009,7 @@ export class GitService extends Disposable { } toggleCodeLens(editor: TextEditor) { - if (this.config.codeLens.visibility === CodeLensVisibility.Off || - (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return; + if (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled) return; Logger.log(`toggleCodeLens()`); if (this._codeLensProviderDisposable) { diff --git a/src/quickPicks/common.ts b/src/quickPicks/common.ts index 10dc8ef..29ca012 100644 --- a/src/quickPicks/common.ts +++ b/src/quickPicks/common.ts @@ -1,8 +1,7 @@ 'use strict'; import { CancellationTokenSource, commands, Disposable, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, TextEditor, Uri, window, workspace } from 'vscode'; import { Commands, Keyboard, KeyboardScope, KeyMapping, Keys, openEditor } from '../commands'; -import { IAdvancedConfig } from '../configuration'; -import { ExtensionKey } from '../constants'; +import { ExtensionKey, IAdvancedConfig } from '../configuration'; import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService'; // import { Logger } from '../logger'; import * as moment from 'moment'; diff --git a/src/system/function.ts b/src/system/function.ts index 3b5f0e0..9ab6d82 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -15,4 +15,8 @@ export namespace Functions { export function once(fn: T): T { return _once(fn); } + + export async function wait(ms: number) { + await new Promise(resolve => setTimeout(resolve, ms)); + } } \ No newline at end of file diff --git a/src/system/object.ts b/src/system/object.ts index cc1029f..01244be 100644 --- a/src/system/object.ts +++ b/src/system/object.ts @@ -55,4 +55,10 @@ export namespace Objects { } } } + + export function* values(o: any): IterableIterator<[any]> { + for (const key in o) { + yield [o[key]]; + } + } } \ No newline at end of file diff --git a/src/system/string.ts b/src/system/string.ts index e9ef1b7..b0a1954 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -1,8 +1,84 @@ 'use strict'; +import { Objects } from './object'; const _escapeRegExp = require('lodash.escaperegexp'); export namespace Strings { export function escapeRegExp(s: string): string { return _escapeRegExp(s); } + + const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g; + const TokenSanitizeRegex = /\$\{(\w*?)(?:\W|\d)*?\}/g; + + export interface ITokenOptions { + padDirection: 'left' | 'right'; + truncateTo: number | undefined; + collapseWhitespace: boolean; + } + + export function getTokensFromTemplate(template: string) { + const tokens: { key: string, options: ITokenOptions }[] = []; + + let match = TokenRegex.exec(template); + while (match != null) { + const truncateTo = match[2]; + const option = match[3]; + tokens.push({ + key: match[1], + options: { + truncateTo: truncateTo == null ? undefined : parseInt(truncateTo, 10), + padDirection: option === '-' ? 'left' : 'right', + collapseWhitespace: option === '?' + } + }); + match = TokenRegex.exec(template); + } + + return tokens; + } + + export function interpolate(template: string, tokens: { [key: string]: any }): string { + return new Function(...Object.keys(tokens), `return \`${template}\`;`)(...Objects.values(tokens)); + } + + export function interpolateLazy(template: string, context: object): string { + template = template.replace(TokenSanitizeRegex, '$${c.$1}'); + return new Function('c', `return \`${template}\`;`)(context); + } + + export function padLeft(s: string, padTo: number, padding: string = '\u00a0') { + const diff = padTo - s.length; + return (diff <= 0) ? s : '\u00a0'.repeat(diff) + s; + } + + export function padLeftOrTruncate(s: string, max: number, padding?: string) { + if (s.length < max) return padLeft(s, max, padding); + if (s.length > max) return truncate(s, max); + return s; + } + + export function padRight(s: string, padTo: number, padding: string = '\u00a0') { + const diff = padTo - s.length; + return (diff <= 0) ? s : s + '\u00a0'.repeat(diff); + } + + export function padOrTruncate(s: string, max: number, padding?: string) { + const left = max < 0; + max = Math.abs(max); + + if (s.length < max) return left ? padLeft(s, max, padding) : padRight(s, max, padding); + if (s.length > max) return truncate(s, max); + return s; + } + + export function padRightOrTruncate(s: string, max: number, padding?: string) { + if (s.length < max) return padRight(s, max, padding); + if (s.length > max) return truncate(s, max); + return s; + } + + export function truncate(s: string, truncateTo?: number) { + if (!s || truncateTo === undefined || s.length <= truncateTo) return s; + return `${s.substring(0, truncateTo - 1)}\u2026`; + } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6a41444..e40fc16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "forceConsistentCasingInFileNames": true, - "lib": [ "es2015" ], + "lib": [ "es2015", "es2016" ], "module": "commonjs", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, @@ -12,7 +12,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "target": "es2015" + "target": "es2016" }, "exclude": [ "node_modules", diff --git a/tslint.json b/tslint.json index bf5beb6..f5c0c8b 100644 --- a/tslint.json +++ b/tslint.json @@ -39,7 +39,7 @@ "ignore-properties" ], "no-internal-module": true, - "no-invalid-template-strings": true, + // "no-invalid-template-strings": true, "no-irregular-whitespace": true, "no-reference": true, "no-string-throw": true,