59 Commits

Author SHA1 Message Date
Eric Amodio
7f2ec30164 Preps v4.1-beta 2017-06-10 04:21:02 -04:00
Eric Amodio
48a1ca704d Adds new recent changes annotations 2017-06-10 04:19:07 -04:00
Eric Amodio
23c7171d7f Preps v4.0.2-beta 2017-06-10 00:57:56 -04:00
Eric Amodio
dd0b95498e Fixes excessive memory usages with diff regex 2017-06-10 00:49:22 -04:00
Eric Amodio
e6316400f0 Optimized parsers for speed & memory usage
Switches to lazy parsing of diff chunks
2017-06-10 00:16:46 -04:00
Eric Amodio
eeff31cf27 Removes I from interface naming of "things" 2017-06-09 18:00:54 -04:00
Eric Amodio
eb3b9ad6c9 Preps v4.0.1 2017-06-09 17:27:54 -04:00
Eric Amodio
10674124c8 Fixes #87 - Can't open files in remote when using git@ urls (ssh) 2017-06-09 17:26:20 -04:00
Eric Amodio
e0f66247cf Preps v4.0.0 2017-06-09 16:35:52 -04:00
Eric Amodio
badd999db1 Defaults toggleWhitespace.enabled to true
Turns off whitespace toggling if already 'none'
2017-06-09 16:32:05 -04:00
Eric Amodio
9ae4cc36a1 Adds ability to suppress most warning messages 2017-06-09 15:42:56 -04:00
Eric Amodio
30bb4398a3 Changes formatting & ordering 2017-06-09 15:41:49 -04:00
Eric Amodio
9c7a971e21 Changes to undefined checks 2017-06-09 15:41:49 -04:00
Eric Amodio
ba59fb29ad Fixes #85 - Show File Commit Details doesn't work on last line if it is empty 2017-06-09 15:41:49 -04:00
Eric Amodio
50ba3e1446 Fixes #84 - diff w/ previous should only compare w/ working tree if file is uncommitted 2017-06-09 15:41:49 -04:00
Eric Amodio
62e5ef6225 Fixes issue with open in remote not showing with no editors 2017-06-09 10:18:10 -04:00
Eric Amodio
ed54d289dd Stops collecting initialized event (data cap issues) 2017-06-09 09:36:00 -04:00
Eric Amodio
2a8dafd9e9 Fixes #83 - Close Unchanged Files command can infinitely loop 2017-06-08 00:54:46 -04:00
Eric Amodio
016c561ead Preps v4.0.0-beta.2 2017-06-07 12:55:07 -04:00
Eric Amodio
9cf86a41ec Adds setting to explicitly control telemetry
Disables zone.js monkey patching by application insights
2017-06-07 12:52:56 -04:00
Eric Amodio
4eb1c3e36a Renames interpolation method 2017-06-07 12:52:56 -04:00
Eric Amodio
95e0a6c71b Adds code lens range to debug info 2017-06-07 12:52:56 -04:00
Eric Amodio
35ca8106c9 Adds customizable code lens strings 2017-06-07 12:52:56 -04:00
Eric Amodio
948a75de79 Don't wait for settings message before continuing 2017-06-07 12:52:56 -04:00
Eric Amodio
4b0891b949 Adds settings migration support 2017-06-07 12:52:56 -04:00
Eric Amodio
a9d94868e7 Preps v4.0.0-beta 2017-06-07 12:52:56 -04:00
Eric Amodio
3c45c7e049 Anchors the code lens to the end of the line 2017-06-07 12:51:00 -04:00
Eric Amodio
ba0d55d5d4 Updates dependencies 2017-06-07 12:50:59 -04:00
Eric Amodio
d2dc172042 Attempts to fix #80 - on line with link, annotation gets underlined 2017-06-07 12:49:52 -04:00
Eric Amodio
e5e582d300 Fixes #81 - Current line annotation is too sticky 2017-06-07 12:49:52 -04:00
Eric Amodio
7c9e4b911c Adds better formatting of settings 2017-06-07 12:49:52 -04:00
Eric Amodio
42fdf9f327 Updates screenshots 2017-06-07 12:49:52 -04:00
Eric Amodio
547d50fed6 Adds another screenshot to README 2017-06-07 12:49:52 -04:00
Eric Amodio
6c33686335 Adds vscode issue TODO 2017-06-07 12:49:52 -04:00
Eric Amodio
28355d41b6 Adds more screenshots to README 2017-06-07 12:49:52 -04:00
Eric Amodio
6e5bb2343e Preps v4.0.0-alpha.2 2017-06-07 12:49:51 -04:00
Eric Amodio
d01c592533 Adds welcome message for first-time users 2017-06-07 12:48:22 -04:00
Eric Amodio
37e48ded2d Major refactor/rework -- many new features and breaking changes
Adds all-new, beautiful, highly customizable and themeable, file blame annotations
Adds all-new configurability and themeability to the current line blame annotations
Adds all-new configurability to the status bar blame information
Adds all-new configurability over which commands are added to which menus via the `gitlens.advanced.menus` setting
Adds better configurability over where Git code lens will be shown -- both by default and per language
Adds an all-new `changes` (diff) hover annotation to the current line - provides instant access to the line's previous version
Adds `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) - toggles the current line blame annotations on and off
Adds `Show Line Blame Annotations` command (`gitlens.showLineBlame`) - shows the current line blame annotations
Adds `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) - toggles the file blame annotations on and off
Adds `Show File Blame Annotations` command (`gitlens.showFileBlame`) - shows the file blame annotations
Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the `editor/title` context menu
Adds `Open Repo in Remote` command (`gitlens.openRepoInRemote`) to the `editor/title` context menu
Changes the position of the `Open File in Remote` command (`gitlens.openFileInRemote`) in the context menus - now in the `navigation` group
Changes the `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) to always toggle the Git code lens on and off
Removes the on-demand `trailing` file blame annotations -- didn't work out and just ended up with a ton of visual noise
Removes `Toggle Blame Annotations` command (`gitlens.toggleBlame`) - replaced by the `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`)
Removes `Show Blame Annotations` command (`gitlens.showBlame`) - replaced by the `Show File Blame Annotations` command (`gitlens.showFileBlame`)
2017-06-07 12:48:22 -04:00
Eric Amodio
e3e7605268 Preps v3.6.1 2017-06-07 12:43:22 -04:00
Eric Amodio
f16c3857e5 Fixes zone.js monkey patching by application-insights 2017-06-07 12:42:33 -04:00
Eric Amodio
5298511bb9 Preps v3.6.0 2017-06-02 18:45:04 -04:00
Eric Amodio
4400ab1da9 Updates dependencies 2017-06-02 15:12:57 -04:00
Eric Amodio
6a9977b954 Fixes issue where the wrong diff line could be shown 2017-06-02 15:12:31 -04:00
Eric Amodio
c0d5f55baa Preps v3.6.0-beta 2017-05-27 04:33:27 -04:00
Eric Amodio
68f6ae8f3a Updates dependencies 2017-05-27 04:32:42 -04:00
Eric Amodio
53c691898f Changes behavior of diffWithWorking to always does what it says
Compares the current file with the working tree -- if the current file *is* the working file, it will show a `File matches the working tree` message
2017-05-27 04:26:51 -04:00
Eric Amodio
365af9c54b Changes behavior of diffWithPrevious to always does what it says
Compares the current file with the previous commit to that file
2017-05-27 04:26:12 -04:00
Eric Amodio
55b1a66ec0 Adds 'gitlens.diffWithWorking' status bar command option
Changes 'gitlens.diffWithPrevious' status bar command option behavior
2017-05-27 04:25:12 -04:00
Eric Amodio
522e5a49a2 Renames 'Compare with *' in quick pick menus
Renames 'Compare File with Previous Commit' command
Renames 'Compare Line with Previous Commit' command
Renames 'Compare Line with Working Tree' command
2017-05-27 04:20:25 -04:00
Eric Amodio
e99febb52d Adds diff info to active line hover always 2017-05-27 02:39:10 -04:00
Eric Amodio
2036c8abaf Preps v3.5.1 2017-05-25 01:39:16 -04:00
Eric Amodio
021a5b833a Fixes #71 - blame invalid on external edit 2017-05-25 01:31:24 -04:00
Eric Amodio
f1042de9c7 Stops some code lens actions when uncommitted 2017-05-25 01:30:28 -04:00
Eric Amodio
efd3d40aa8 Switches to use GitUris in more places 2017-05-25 01:05:18 -04:00
Eric Amodio
9c7062020e Fixes issue with blame on versioned files
Stops falling back to the cached blame of the working file
Handles git scheme urls properly
2017-05-25 00:49:36 -04:00
Eric Amodio
9da80c121b Debounces other active line events 2017-05-25 00:35:12 -04:00
Eric Amodio
5380724323 Preps v3.5.0 2017-05-24 23:01:28 -04:00
Eric Amodio
77651701aa Removes insiders flag from stash commands 2017-05-24 22:59:41 -04:00
Eric Amodio
bb834f2e0a Removes insiders flag from remotes 2017-05-24 22:58:54 -04:00
101 changed files with 6099 additions and 2318 deletions

View File

@@ -4,26 +4,114 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [3.5.0-beta.2] - 2017-05-23 ## [4.1.0-beta] - 2017-06-10
### Fixed ### Added
- Fixes [#40](https://github.com/eamodio/vscode-gitlens/issues/40) - Encoding issues - Adds all-new recent changes annotations of the whole-file - annotates and highlights all of lines changed in the most recent commit
- Given the limitations of the vscode api, I'm unable to fix all the encoding issues, but many of them should now be squashed - Can customize the [layout](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#file-recent-changes-annotation-settings), as well as the [theme](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#theme-settings)
- `files.encoding` is now honored for the cases where the encoding cannot currently be gleaned - Adds `Toggle Recent File Changes Annotations` command (`gitlens.toggleFileRecentChanges`) - toggles the recent changes annotations on and off
- Improves performance
- Optimized git output parsing to increase speed and reduce memory usage
- Defers diff chunk parsing until it is actually required
## [3.5.0-beta] - 2017-05-23 ### Fixed
- Fixes excessive memory usage when parsing diffs
## [4.0.1] - 2017-06-09
### Fixed
- Fixes [#87](https://github.com/eamodio/vscode-gitlens/issues/87) - Can't open files in remote when using git@ urls (ssh)
## [4.0.0] - 2017-06-09
### Added
- Adds all-new, beautiful, highly customizable and themeable, file blame annotations
- Can now fully customize the [layout and content](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#file-blame-annotation-settings), as well as the [theme](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#theme-settings)
- Adds all-new configurability and themeability to the current line blame annotations
- Can now fully customize the [layout and content](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#line-blame-annotation-settings), as well as the [theme](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#theme-settings)
- Adds all-new configurability to the status bar blame information
- Can now fully customize the [layout and content](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#status-bar-settings)
- Adds all-new [configurability](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#advanced-settings) over which commands are added to which menus via the `gitlens.advanced.menus` setting
- Adds better [configurability](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#code-lens-settings) over where Git code lens will be shown -- both by default and per language
- Adds an all-new `changes` (diff) hover annotation to the current line - provides instant access to the line's previous version
- Adds `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) - toggles the current line blame annotations on and off
- Adds `Show Line Blame Annotations` command (`gitlens.showLineBlame`) - shows the current line blame annotations
- Adds `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) - toggles the file blame annotations on and off
- Adds `Show File Blame Annotations` command (`gitlens.showFileBlame`) - shows the file blame annotations
- Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the `editor/title` context menu
- Adds `Open Repo in Remote` command (`gitlens.openRepoInRemote`) to the `editor/title` context menu
- Adds `gitlens.strings.*` settings to allow for the customization of certain strings displayed
- Adds `gitlens.theme.*` settings to allow for the theming of certain elements
- Adds `gitlens.advanced.telemetry.enabled` settings to explicitly opt-in or out of telemetry, but still ultimately honors the `telemetry.enableTelemetry` setting
- Adds ability to suppress most warning messages - which can be re-enabled using the `Reset Suppressed Warnings` command (`gitlens.resetSuppressedWarnings`)
### Changed
- (BREAKING) Almost all of the GitLens settings have either been renamed, removed, or otherwise changed - see the [README](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens#extension-settings)`
- Changes the positioning of the Git code lens to try to be at the end of any other code lens on the same line
- Changes the position of the `Open File in Remote` command (`gitlens.openFileInRemote`) in the context menus - now in the `navigation` group
- Changes the `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) to always toggle the Git code lens on and off
- Changes the default of `gitlens.advanced.toggleWhitespace.enabled` back to `true`, but automatically disables whitespace toggling if whitespace rendering is not on
### Removed
- Removes the on-demand `trailing` file blame annotations -- didn't work out and just ended up with a ton of visual noise
- Removes `Toggle Blame Annotations` command (`gitlens.toggleBlame`) - replaced by the `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`)
- Removes `Show Blame Annotations` command (`gitlens.showBlame`) - replaced by the `Show File Blame Annotations` command (`gitlens.showFileBlame`)
### Fixed
- Fixes [#81](https://github.com/eamodio/vscode-gitlens/issues/81) - Current line annotation feels too sticky
- Fixes [#83](https://github.com/eamodio/vscode-gitlens/issues/83) - Calling "close unchanged files" results in no new files being openable
- Fixes issues with the zone.js monkey patching done by application insights (telemetry) - disables all the monkey patching
- Fixes issue with `Open Branch in Remote` & `Open Repository in Remote` not showing when there are no open editors
## [3.6.1] - 2017-06-07
### Fixed
- Fixes issues with the zone.js monkey patching done by application insights (telemetry) - disables all the monkey patching
## [3.6.0] - 2017-06-02
### Added
- Adds diff information (the line's previous version) into the active line hover
- Adds a `gitlens.diffWithWorking` status bar command option - compares the current line commit with the working tree
### Changed
- Changes the behavior of the `Compare File with Working Tree` command (`gitlens.diffWithWorking`) - always does what it says :)
- Compares the current file with the working tree -- if the current file *is* the working file, it will show a `File matches the working tree` message
- Changes the behavior of the `Compare File with Previous` command (`gitlens.diffWithPrevious`) - always does what it says :)
- Compares the current file with the previous commit to that file
- Changes the behavior of the `gitlens.diffWithPrevious` status bar command option - compares the current line commit with the previous
- Renames `Compare File with Previous Commit` command to `Compare File with Previous`
- Renames `Compare Line with Previous Commit` command to `Compare Line Commit with Previous`
- Renames `Compare Line with Working Tree` command to `Compare Line Commit with Working Tree`
- Renames `Compare with Previous Commit` in quick pick menus to `Compare File with Previous`
- Renames `Compare with Working Tree` in quick pick menus to `Compare File with Working Tree`
### Fixed
- Fixes [#79](https://github.com/eamodio/vscode-gitlens/issues/79) - Application insights package breaks GitLens + eslint
## [3.5.1] - 2017-05-25
### Changed
- Changes certain code lens actions to be unavailable (unclickable) when the commit referenced is uncommitted - avoids unwanted error messages
- Debounces more events when tracking the active line to further reduce lag
### Fixed
- Fixes [#71](https://github.com/eamodio/vscode-gitlens/issues/71) - Blame information is invalid when a file has changed outside of vscode
- Fixes issue with showing the incorrect blame for versioned files (i.e. files on the left of a diff, etc)
## [3.5.0] - 2017-05-24
### Added ### Added
- Improves performance - Improves performance
- Reduces the number of git calls on known "untrackables" - Reduces the number of git calls on known "untrackables"
- Caches many more git commands to reduce git command roundtrips and parsing - Caches many more git commands to reduce git command roundtrips and parsing
- Increases the debounce (delay) on cursor movement to reduce lag when navigating around a file - Increases the debounce (delay) on cursor movement to reduce lag when navigating around a file
- Adds diff information (previous line's code) into the active line hover when the current line is uncommitted - Adds diff information (the line's previous version) into the active line hover when the current line is uncommitted
- Adds `gitlens.statusBar.alignment` settings to control the alignment of the status bar -- thanks to [PR #72](https://github.com/eamodio/vscode-gitlens/pull/72) by Zack Schuster ([@zackschuster](https://github.com/zackschuster))! - Adds `gitlens.statusBar.alignment` settings to control the alignment of the status bar -- thanks to [PR #72](https://github.com/eamodio/vscode-gitlens/pull/72) by Zack Schuster ([@zackschuster](https://github.com/zackschuster))!
- Adds `Open Branch in Remote` command (`gitlens.openBranchInRemote`) - opens the current branch commits in the supported remote service - Adds `Open Branch in Remote` command (`gitlens.openBranchInRemote`) - opens the current branch commits in the supported remote service
- Adds `Open Repository in Remote` command (`gitlens.openRepoInRemote`) - opens the repository in the supported remote service - Adds `Open Repository in Remote` command (`gitlens.openRepoInRemote`) - opens the repository in the supported remote service
- Adds `Stash Changes` option to stashed changes quick pick menu -- no longer hidden behind the `"gitlens.insiders": true` setting
- Adds `Stash Unstaged Changes` option to stashed changes quick pick menu -- no longer hidden behind the `"gitlens.insiders": true` setting
- Adds `Apply Stashed Changes` command (`gitlens.stashApply`) to apply the selected stashed changes to the working tree -- no longer hidden behind the `"gitlens.insiders": true` setting
- Adds `Stash Changes` command (`gitlens.stashSave`) to stash any working tree changes -- no longer hidden behind the `"gitlens.insiders": true` setting
- Adds support to the `Search commits` command (`gitlens.showCommitSearch`) to work without any active editor - Adds support to the `Search commits` command (`gitlens.showCommitSearch`) to work without any active editor
- Adds commit search pre-population -- if there is an active editor it will use the commit sha of the current line commit, otherwise it will use the current clipboard - Adds commit search pre-population -- if there is an active editor it will use the commit sha of the current line commit, otherwise it will use the current clipboard
### Changed ### Changed
- Changes `Open File in Remote` and `Open Line Commit in Remote` commands to actually work for everyone (part of their implementation was still behind the `gitlens.insiders` setting)
- Changes the active line hover to only show at the beginning and end of a line if `gitlens.blame.annotation.activeLine` is `both` - Changes the active line hover to only show at the beginning and end of a line if `gitlens.blame.annotation.activeLine` is `both`
- Changes `alt+f` shortcut to `alt+/` for the `Search commits` command (`gitlens.showCommitSearch`) - Changes `alt+f` shortcut to `alt+/` for the `Search commits` command (`gitlens.showCommitSearch`)
- Changes `alt+right` on commit details quick pick menu to execute the `Compare File with Previous Commit` command (`gitlens.diffWithPrevious`) when a file is selected - Changes `alt+right` on commit details quick pick menu to execute the `Compare File with Previous Commit` command (`gitlens.diffWithPrevious`) when a file is selected
@@ -32,6 +120,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Fixed ### Fixed
- Fixes [#73](https://github.com/eamodio/vscode-gitlens/issues/73) - GitLens doesn't work with Chinese filenames - Fixes [#73](https://github.com/eamodio/vscode-gitlens/issues/73) - GitLens doesn't work with Chinese filenames
- Fixes [#40](https://github.com/eamodio/vscode-gitlens/issues/40) - Encoding issues
- Given the limitations of the vscode api, I'm unable to fix all the encoding issues, but many of them should now be squashed
- `files.encoding` is now honored for the cases where the encoding cannot currently be gleaned
- Fixes incorrect file selection from the commit details quick pick menu - Fixes incorrect file selection from the commit details quick pick menu
- Fixes incorrect command execution when using `"gitlens.statusBar.command": "gitlens.showQuickRepoHistory"` - Fixes incorrect command execution when using `"gitlens.statusBar.command": "gitlens.showQuickRepoHistory"`
- Fixes a bunch of issues that were revealed by enabling Typescript `strict` mode - Fixes a bunch of issues that were revealed by enabling Typescript `strict` mode
@@ -65,7 +156,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [3.4.5] - 2017-04-13 ## [3.4.5] - 2017-04-13
### Added ### Added
- Completely overhauls the [GitLens documentation](https://github.com/eamodio/vscode-gitlens/blob/master/README.md) and messaging -- make sure to check it out to see all the powerful features GitLen provides! - Completely overhauls the [GitLens documentation](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) and messaging -- make sure to check it out to see all the powerful features GitLen provides!
- Adds `gitlens.blame.annotation.activeLineDarkColor` & `gitlens.blame.annotation.activeLineLightColor` settings to control the colors of the active line blame annotation - Adds `gitlens.blame.annotation.activeLineDarkColor` & `gitlens.blame.annotation.activeLineLightColor` settings to control the colors of the active line blame annotation
### Changed ### Changed

251
README.md
View File

@@ -5,58 +5,84 @@
# GitLens # 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 information (author and date, by default) 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 ### Preview — featuring blame annotations, code lens, status bar details, quick pick menus for navigation and exploration, compare with previous, and more
#### Featuring code lens, whole file inline blame annotations, and navigation and exploration via quick pick menus ![GitLens preview](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/gitlens-preview.gif)
![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
![GitLens preview 2](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview2.gif)
## Features ## Features
#### Git Blame Annotations ### Git Blame Annotations
- Adds a **blame annotation** to the end of the selected line showing the commit id and message, with more details in a hover popup ([optional](#extension-settings), on by default) - Adds an unobtrusive, highly [customizable](#line-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotation** to the end of the current line ([optional](#line-blame-annotation-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 ![Line Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotation.png)
- Also adds a `Show Blame Annotations` command (`gitlens.showBlame`) - Contains the author, date, and message of the line's most recent commit, by [default](#line-blame-annotation-settings)
- Also adds a `details` hover annotation to the current line annotation which provides more commit details ([optional](#line-blame-annotation-settings), on by default)
- Also adds a `changes` (diff) hover annotation to the current line annotation which provides **instant** access to the line's previous version ([optional](#line-blame-annotation-settings), on by default)
- Adds **author and date blame information** about the selected line to the **status bar** ([optional](#extension-settings), on by default) ![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png)
- 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 - Adds on-demand, beautiful, highly [customizable](#file-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotations** of the whole file
- 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 ![File Blame Annotation](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-file-blame-annotations.png)
- Compare the file with the previous commit - Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings)
- Show a quick pick menu with details and commands for the commit - Contains the commit message and date, by [default](#file-blame-annotation-settings)
- Also adds a `details` hover annotation to the line's annotation which provides more commit details ([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)
![Status Bar Blame](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-status-bar.png)
- 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 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 file
- Show a quick pick menu with the commit history of the current branch - Show a quick pick menu with the commit history of the current branch
#### Git Code Lens - 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 **code lens** to the top of the file and on code blocks ([optional](#extension-settings), on by default) - 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 Recent Changes Annotations
- Adds on-demand, [customizable](#file-recent-changes-annotation-settings) and [themeable](#theme-settings), **recent changes annotations** of the whole file
- Highlights all of lines changed in the most recent commit
- Also adds a `changes` (diff) hover annotation to the current line annotation which provides **instant** access to the line's previous version ([optional](#file-recent-changes-annotation-settings), on by default)
- Adds `Toggle Recent File Changes Annotations` command (`gitlens.toggleFileRecentChanges`) to toggle the recent changes annotations on and off
### Git Code Lens
- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default)
![Git Code Lens](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-code-lens.png)
- **Recent Change** — author and date of the most recent commit for the file or code block - **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) - **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 - 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 - Provides [customizable](#code-lens-settings) click behavior for each code lens — choose between one of the following
- Toggle whole file blame annotations on and off - Toggle file blame annotations on and off
- Compare the file with the previous commit - 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 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 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 file
- Show a quick pick menu with the commit history of the current branch - 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 ### Powerful Comparison Tools
- Effortlessly navigate between comparisions via the `alt+,` and `alt+.` shortcut keys to go back and forth through a file's revisions - Effortlessly navigate between comparisions via the `alt+,` and `alt+.` shortcut keys to go back and forth through a file's revisions
@@ -68,15 +94,15 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Compare File with Next Commit` command (`gitlens.diffWithNext`) with a shortcut of `alt+.` to compare the active file/diff with the next commit revision - Adds a `Compare File with Next Commit` command (`gitlens.diffWithNext`) with a shortcut of `alt+.` to compare the active file/diff with the next commit revision
- Adds a `Compare File with Previous Commit` command (`gitlens.diffWithPrevious`) with a shortcut of `alt+,` to compare the active file/diff with the previous commit revision - Adds a `Compare File with Previous` command (`gitlens.diffWithPrevious`) with a shortcut of `alt+,` to compare the active file/diff with the previous commit revision
- Adds a `Compare Line with Previous Commit` command (`gitlens.diffLineWithPrevious`) with a shortcut of `shift+alt+,` to compare the active file/diff with the previous line commit revision - Adds a `Compare Line Commit with Previous` command (`gitlens.diffLineWithPrevious`) with a shortcut of `shift+alt+,` to compare the active file/diff with the previous line commit revision
- Adds a `Compare File with Working Tree` command (`gitlens.diffWithWorking`) with a shortcut of `shift+alt+w` to compare the most recent commit revision of the active file/diff with the working tree - Adds a `Compare File with Working Tree` command (`gitlens.diffWithWorking`) with a shortcut of `shift+alt+w` to compare the most recent commit revision of the active file/diff with the working tree
- Adds a `Compare Line with Working Tree` command (`gitlens.diffLineWithWorking`) with a shortcut of `alt+w` to compare the commit revision of the active line with the working tree - Adds a `Compare Line Commit with Working Tree` command (`gitlens.diffLineWithWorking`) with a shortcut of `alt+w` to compare the commit revision of the active line with the working tree
#### Navigate and Explore ### Navigate and Explore
- Adds a `Search Commits` command (`gitlens.showCommitSearch`) with a shortcut of `alt+/` to search for commits by message, author, file(s), or commit id - Adds a `Search Commits` command (`gitlens.showCommitSearch`) with a shortcut of `alt+/` to search for commits by message, author, file(s), or commit id
@@ -88,7 +114,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Show Current Branch History` command (`gitlens.showQuickRepoHistory`) with a shortcut of `shift+alt+h` to show a paged **branch history quick pick menu** of the current branch for exploring its commit history - Adds a `Show Current Branch History` command (`gitlens.showQuickRepoHistory`) with a shortcut of `shift+alt+h` to show a paged **branch history quick pick menu** of the current branch for exploring its commit history
![Branch History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-branch-history.png) ![Branch History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-branch-history.png)
- Provides entries to `Show Commit Search` and `Open Branch in <remote-service>` when available - Provides entries to `Show Commit Search` and `Open Branch in <remote-service>` when available
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
@@ -99,7 +125,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Show File History` command (`gitlens.showQuickFileHistory`) to show a paged **file history quick pick menu** of the active file for exploring its commit history - Adds a `Show File History` command (`gitlens.showQuickFileHistory`) to show a paged **file history quick pick menu** of the active file for exploring its commit history
![File History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-file-history.png) ![File History Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-file-history.png)
- Provides entries to `Show Branch History` and `Open File in <remote-service>` when available - Provides entries to `Show Branch History` and `Open File in <remote-service>` when available
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
@@ -107,7 +133,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Show Commit Details` command (`gitlens.showQuickCommitDetails`) to show a **commit details quick pick menu** of the most recent commit of the active file - Adds a `Show Commit Details` command (`gitlens.showQuickCommitDetails`) to show a **commit details quick pick menu** of the most recent commit of the active file
![Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-commit-details.png) ![Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-commit-details.png)
- Quickly see the set of files changed in the commit, complete with status indicators for adds, changes, renames, and deletes - Quickly see the set of files changed in the commit, complete with status indicators for adds, changes, renames, and deletes
- Provides entries to `Copy to Clipboard`, `Directory Compare`, `Open Changed Files`, `Open File in <remote-service>` when available, and more - Provides entries to `Copy to Clipboard`, `Directory Compare`, `Open Changed Files`, `Open File in <remote-service>` when available, and more
@@ -118,15 +144,15 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Show Line Commit Details` command (`gitlens.showQuickCommitFileDetails`) with a shortcut of `alt+c` to show a **file commit details quick pick menu** of the most recent commit of the active file - Adds a `Show Line Commit Details` command (`gitlens.showQuickCommitFileDetails`) with a shortcut of `alt+c` to show a **file commit details quick pick menu** of the most recent commit of the active file
![Line Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-commit-file-details.png) ![Line Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-commit-file-details.png)
- Provides entries to `Show Commit Details`, `Show File History`, `Compare with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more - Provides entries to `Show Commit Details`, `Show File History`, `Compare File with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- Use the `alt+right arrow` shortcut on an entry to execute it without closing the quick pick menu, if possible — commands that open windows outside of VS Code will still close the quick pick menu unless [`"gitlens.advanced.quickPick.closeOnFocusOut": false`](#extension-settings) is set - Use the `alt+right arrow` shortcut on an entry to execute it without closing the quick pick menu, if possible — commands that open windows outside of VS Code will still close the quick pick menu unless [`"gitlens.advanced.quickPick.closeOnFocusOut": false`](#extension-settings) is set
- Adds a `Show Repository Status` command (`gitlens.showQuickRepoStatus`) with a shortcut of `alt+s` to show a **repository status quick pick menu** for visualizing the current repository status - Adds a `Show Repository Status` command (`gitlens.showQuickRepoStatus`) with a shortcut of `alt+s` to show a **repository status quick pick menu** for visualizing the current repository status
![Repository Status Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-repo-status.png) ![Repository Status Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-repo-status.png)
- Quickly see upstream status (if an Git upstream is configured) — complete with ahead and behind information - Quickly see upstream status (if an Git upstream is configured) — complete with ahead and behind information
- If you are ahead of the upstream, an entry will be shown with the number of commits ahead. Chosing it will show a limited **branch history quick pick menu** containing just the commits ahead of the upstream - If you are ahead of the upstream, an entry will be shown with the number of commits ahead. Chosing it will show a limited **branch history quick pick menu** containing just the commits ahead of the upstream
@@ -139,18 +165,18 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Show Stashed Changes` command (`gitlens.showQuickStashList`) to show a **stashed changes quick pick menu** for exploring your repository stash history - Adds a `Show Stashed Changes` command (`gitlens.showQuickStashList`) to show a **stashed changes quick pick menu** for exploring your repository stash history
![Stashed Changes Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-stash-list.png) ![Stashed Changes Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-stash-list.png)
- [Insiders only](#insiders) — Provides entries to `Stash Changes` - Provides entries to `Stash Changes`
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- Chosing a stash entry shows a **stash details quick pick menu** which is very similar to the **commit details quick pick menu** above - Chosing a stash entry shows a **stash details quick pick menu** which is very similar to the **commit details quick pick menu** above
![Stash Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-stash-details.png) ![Stash Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-stash-details.png)
- Quickly see the set of files changed in the stash, complete with status indicators for adds, changes, renames, and deletes - Quickly see the set of files changed in the stash, complete with status indicators for adds, changes, renames, and deletes
- Provides entries to `Copy Message to Clipboard`, `Directory Compare`, and `Open Changed Files` - Provides entries to `Copy Message to Clipboard`, `Directory Compare`, and `Open Changed Files`
- [Insiders only](#insiders) — Provides entries to `Apply Stashed Changes` and `Delete Stashed Changes` — both require a confirmation - Provides entries to `Apply Stashed Changes` and `Delete Stashed Changes` — both require a confirmation
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available - Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- Use the `alt+right arrow` shortcut on an entry to execute it without closing the quick pick menu, if possible — commands that open windows outside of VS Code will still close the quick pick menu unless [`"gitlens.advanced.quickPick.closeOnFocusOut": false`](#extension-settings) is set - Use the `alt+right arrow` shortcut on an entry to execute it without closing the quick pick menu, if possible — commands that open windows outside of VS Code will still close the quick pick menu unless [`"gitlens.advanced.quickPick.closeOnFocusOut": false`](#extension-settings) is set
- Use the `alt+right arrow` shortcut on a file entry in the `Changed Files` section to preview the current revision of the while leaving the quick pick menu open - Use the `alt+right arrow` shortcut on a file entry in the `Changed Files` section to preview the current revision of the while leaving the quick pick menu open
@@ -164,7 +190,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Open Blame History Explorer` command (`gitlens.showBlameHistory`) to show a **blame history explorer** (peek style) to visualize the blame history of a file or code block - Adds a `Open Blame History Explorer` command (`gitlens.showBlameHistory`) to show a **blame history explorer** (peek style) to visualize the blame history of a file or code block
- Likely to be deprecated in a future release, add your voice to [#66](https://github.com/eamodio/vscode-gitlens/issues/66) if you feel it should not be removed - Likely to be deprecated in a future release, add your voice to [#66](https://github.com/eamodio/vscode-gitlens/issues/66) if you feel it should not be removed
#### And More ### And More
- Adds a `Copy Commit ID to Clipboard` command (`gitlens.copyShaToClipboard`) to copy the commit id (sha) of the active line to the clipboard - Adds a `Copy Commit ID to Clipboard` command (`gitlens.copyShaToClipboard`) to copy the commit id (sha) of the active line to the clipboard
@@ -174,46 +200,131 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- Adds a `Close Unchanged Files` command (`gitlens.closeUnchangedFiles`) to close any files without working tree changes - Adds a `Close Unchanged Files` command (`gitlens.closeUnchangedFiles`) to close any files without working tree changes
- [Insiders only](#insiders) — Adds a `Apply Stashed Changes` command (`gitlens.stashApply`) to chose a stash entry to apply to the working tree from a quick pick menu - Adds a `Apply Stashed Changes` command (`gitlens.stashApply`) to chose a stash entry to apply to the working tree from a quick pick menu
- [Insiders only](#insiders) — Adds a `Stash Changes` command (`gitlens.stashSave`) to save any working tree changes to the stash — can optionally provide a stash message - Adds a `Stash Changes` command (`gitlens.stashSave`) to save any working tree changes to the stash — can optionally provide a stash message
## Insiders ## 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 ## Extension Settings
GitLens is highly customizable and provides many configuration settings to allow the personalization of almost all features GitLens is highly customizable and provides many configuration settings to allow the personalization of almost all features
### General Settings
|Name | Description |Name | Description
|-----|------------ |-----|------------
|`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features |`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.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 ### Blame Annotation Settings
|`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 #### File Blame Annotation Settings
|`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 |Name | Description
|`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.file.annotationType`|Specifies the type of blame annotations that will be shown for the current file<br />`gutter` - adds an annotation to the beginning of each line<br />`hover` - shows annotations when hovering over each line
|`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.file.lineHighlight.enabled`|Specifies whether or not to highlight lines associated with the current line
|`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.blame.file.lineHighlight.locations`|Specifies where the associated line highlights will be shown<br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`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.annotations.file.gutter.format`|Specifies the format of the gutter blame annotations<br />Available tokens<br />`${id}` - commit id<br />`${author}` - commit author<br />`${message}` - commit message<br />`${ago}` - relative commit date (e.g. 1 day ago)<br />`${date}` - formatted commit date (format specified by `gitlens.annotations.file.gutter.dateFormat`)<br />`${authorAgo}` - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting
|`gitlens.codeLens.authors.enabled`|Specifies whether the authors code lens is shown |`gitlens.annotations.file.gutter.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations<br />See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`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.annotations.file.gutter.compact`|Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations
|`gitlens.codeLens.recentChange.enabled`|Specifies whether the recent change code lens is shown |`gitlens.annotations.file.gutter.heatmap.enabled`|Specifies whether or not to provide a heatmap indicator in the gutter blame annotations
|`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.annotations.file.gutter.heatmap.location`|Specifies where the heatmap indicators will be shown in the gutter blame annotations<br />`left` - adds a heatmap indicator on the left edge of the gutter blame annotations<br />`right` - adds a heatmap indicator on the right edge of the gutter blame annotations
|`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.annotations.file.gutter.hover.details`|Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations
|`gitlens.codeLens.locationCustomSymbols`|Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind` |`gitlens.annotations.file.gutter.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.codeLens.languageLocations`|Specifies where code lens will be rendered in the active document for the specified languages |`gitlens.annotations.file.hover.heatmap.enabled`|Specifies whether or not to provide heatmap indicators on the left edge of each line
|`gitlens.menus.diff.enabled`|Specifies whether diff commands will be added to the context menus |`gitlens.annotations.file.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.statusBar.enabled`|Specifies whether blame information is shown in the status bar
|`gitlens.statusBar.alignment`|Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right #### Line Blame Annotation Settings
|`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 committed file with the previous commit. `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` |Name | Description
|`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.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<br />`trailing` - adds an annotation to the end of the current line<br />`hover` - shows annotations when hovering over the current line
|`gitlens.annotations.line.trailing.format`|Specifies the format of the trailing blame annotations<br />Available tokens<br />`${id}` - commit id<br />`${author}` - commit author<br />`${message}` - commit message<br />`${ago}` - relative commit date (e.g. 1 day ago)<br />`${date}` - formatted commit date (format specified by `gitlens.annotations.line.trailing.dateFormat`)<br />`${authorAgo}` - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting
|`gitlens.annotations.line.trailing.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations<br />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
### File Recent Changes Annotation Settings
|Name | Description
|-----|------------
|`gitlens.recentChanges.file.lineHighlight.locations`|Specifies where the highlights of the recently changed lines will be shown<br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`gitlens.annotations.file.recentChanges.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotations
|`gitlens.annotations.file.recentChanges.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole 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<br />`gitlens.toggleFileBlame` - toggles file blame annotations<br />`gitlens.showBlameHistory` - opens the blame history explorer<br />`gitlens.showFileHistory` - opens the file history explorer<br />`gitlens.diffWithPrevious` - compares the current committed file with the previous commit<br />`gitlens.showQuickCommitDetails` - shows a commit details quick pick<br />`gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick<br />`gitlens.showQuickFileHistory` - shows a file history quick pick<br />`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<br />`gitlens.toggleFileBlame` - toggles file blame annotations<br />`gitlens.showBlameHistory` - opens the blame history explorer<br />`gitlens.showFileHistory` - opens the file history explorer<br />`gitlens.diffWithPrevious` - compares the current committed file with the previous commit<br />`gitlens.showQuickCommitDetails` - shows a commit details quick pick<br />`gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick<br />`gitlens.showQuickFileHistory` - shows a file history quick pick<br />`gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.locations`|Specifies where Git code lens will be shown in the document<br />`document` - adds code lens at the top of the document<br />`containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc)<br />`blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines<br />`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<br />`left` - align to the left, `right` - align to the right
|`gitlens.statusBar.command`|Specifies the command to be executed when the blame status bar item is clicked<br />`gitlens.toggleFileBlame` - toggles file blame annotations<br />`gitlens.showBlameHistory` - opens the blame history explorer<br />`gitlens.showFileHistory` - opens the file history explorer<br />`gitlens.diffWithPrevious` - compares the current line commit with the previous<br />`gitlens.diffWithWorking` - compares the current line commit with the working tree<br />`gitlens.toggleCodeLens` - toggles Git code lens<br />`gitlens.showQuickCommitDetails` - shows a commit details quick pick<br />`gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick<br />`gitlens.showQuickFileHistory` - shows a file history quick pick<br />`gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.statusBar.format`|Specifies the format of the blame information on the status bar<br />Available tokens<br />`${id}` - commit id<br />`${author}` - commit author<br />`${message}` - commit message<br />`${ago}` - relative commit date (e.g. 1 day ago)<br />`${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting
|`gitlens.statusBar.dateFormat`|Specifies the date format of absolute dates shown in the blame information on the status bar<br />See https://momentjs.com/docs/#/displaying/format/ for valid formats
### Strings Settings
|Name | Description
|-----|------------
|`gitlens.strings.codeLens.unsavedChanges.recentChangeAndAuthors`|Specifies the string to be shown in place of both the `recent change` and `authors` code lens when there are unsaved changes
|`gitlens.strings.codeLens.unsavedChanges.recentChangeOnly`|Specifies the string to be shown in place of the `recent change` code lens when there are unsaved changes
|`gitlens.strings.codeLens.unsavedChanges.authorsOnly`|Specifies the string to be shown in place of the `authors` code lens when there are unsaved changes
### 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.telemetry.enabled`|Specifies whether or not to enable GitLens telemetry (even if enabled still abides by the overall `telemetry.enableTelemetry` setting
|`gitlens.advanced.menus`|Specifies which commands will be added to which 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 ## Known Issues

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" viewBox="0 0 18 18" xml:space="preserve"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" viewBox="0 0 18 18" xml:space="preserve">
<g> <g>
<rect fill="#FFFFFF" fill-opacity="0.75" x="1" y="0" width="4" height="18"/> <rect fill="#00bcf2" fill-opacity="0.6" x="7" y="0" width="3" height="18"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" viewBox="0 0 18 18" xml:space="preserve"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" viewBox="0 0 18 18" xml:space="preserve">
<g> <g>
<rect fill="#000000" fill-opacity="0.75" x="1" y="0" width="4" height="18"/> <rect fill="#00bcf2" fill-opacity="0.6" x="7" y="0" width="3" height="18"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

BIN
images/gitlens-preview.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

1940
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,9 +16,9 @@ interface AutoCollectConsole {
} }
interface AutoCollectExceptions { interface AutoCollectExceptions {
constructor(client:Client): AutoCollectExceptions; constructor(client: Client): AutoCollectExceptions;
isInitialized(): boolean; isInitialized(): boolean;
enable(isEnabled:boolean): void; enable(isEnabled: boolean): void;
} }
interface AutoCollectPerformance { interface AutoCollectPerformance {
@@ -348,7 +348,7 @@ interface Client {
* @param max the max sample for this set * @param max the max sample for this set
* @param stdDev the standard deviation of the set * @param stdDev the standard deviation of the set
*/ */
trackMetric(name: string, value: number, count?:number, min?: number, max?: number, stdDev?: number, properties?: { trackMetric(name: string, value: number, count?: number, min?: number, max?: number, stdDev?: number, properties?: {
[key: string]: string; [key: string]: string;
}): void; }): void;
@@ -374,7 +374,8 @@ interface Client {
* @param error An error that was returned for this request if it was unsuccessful. Defaults to null. * @param error An error that was returned for this request if it was unsuccessful. Defaults to null.
*/ */
trackRequestSync(request: any /*http.IncomingMessage */, response: any /*http.ServerResponse */, ellapsedMilliseconds?: number, properties?: { trackRequestSync(request: any /*http.IncomingMessage */, response: any /*http.ServerResponse */, ellapsedMilliseconds?: number, properties?: {
[key: string]: string;}, error?: any) : void; [key: string]: string;
}, error?: any): void;
/** /**
* Log information about a dependency of your app. Typically used to track the time database calls or outgoing http requests take from your server. * Log information about a dependency of your app. Typically used to track the time database calls or outgoing http requests take from your server.
@@ -503,6 +504,11 @@ interface ApplicationInsights {
* *
*/ */
setOfflineMode(value: boolean, resentIntervall?: number): ApplicationInsights; setOfflineMode(value: boolean, resentIntervall?: number): ApplicationInsights;
/**
*
*/
setAutoDependencyCorrelation(value: boolean): ApplicationInsights;
} }
declare module "applicationinsights" { declare module "applicationinsights" {

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
import { Functions } from './system';
import { commands, Disposable, TextEditor, window } from 'vscode'; import { commands, Disposable, TextEditor, window } from 'vscode';
import { BuiltInCommands } from './constants'; import { BuiltInCommands } from './constants';
@@ -11,19 +12,20 @@ export class ActiveEditorTracker extends Disposable {
constructor() { constructor() {
super(() => this.dispose()); super(() => this.dispose());
this._disposable = window.onDidChangeActiveTextEditor(e => this._resolver && this._resolver(e)); const fn = Functions.debounce((e: TextEditor) => this._resolver && this._resolver(e), 50);
this._disposable = window.onDidChangeActiveTextEditor(fn);
} }
dispose() { dispose() {
this._disposable && this._disposable.dispose(); this._disposable && this._disposable.dispose();
} }
async awaitClose(timeout: number = 500): Promise<TextEditor> { async awaitClose(timeout: number = 500): Promise<TextEditor | undefined> {
this.close(); this.close();
return this.wait(timeout); return this.wait(timeout);
} }
async awaitNext(timeout: number = 500): Promise<TextEditor> { async awaitNext(timeout: number = 500): Promise<TextEditor | undefined> {
this.next(); this.next();
return this.wait(timeout); return this.wait(timeout);
} }
@@ -36,15 +38,15 @@ export class ActiveEditorTracker extends Disposable {
return commands.executeCommand(BuiltInCommands.NextEditor); return commands.executeCommand(BuiltInCommands.NextEditor);
} }
async wait(timeout: number = 500): Promise<TextEditor> { async wait(timeout: number = 500): Promise<TextEditor | undefined> {
const editor = await new Promise<TextEditor>((resolve, reject) => { const editor = await new Promise<TextEditor>((resolve, reject) => {
let timer: any; let timer: any;
this._resolver = (editor: TextEditor) => { this._resolver = (e: TextEditor) => {
if (timer) { if (timer) {
clearTimeout(timer as any); clearTimeout(timer as any);
timer = 0; timer = 0;
resolve(editor); resolve(e);
} }
}; };
@@ -53,6 +55,7 @@ export class ActiveEditorTracker extends Disposable {
timer = 0; timer = 0;
}, timeout) as any; }, timeout) as any;
}); });
this._resolver = undefined; this._resolver = undefined;
return editor; return editor;
} }

View File

@@ -0,0 +1,350 @@
'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 { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
import { Logger } from '../logger';
import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider';
import { WhitespaceController } from './whitespaceController';
export type FileAnnotationType = 'gutter' | 'hover' | 'recentChanges';
export const FileAnnotationType = {
Gutter: 'gutter' as FileAnnotationType,
Hover: 'hover' as FileAnnotationType,
RecentChanges: 'recentChanges' as FileAnnotationType
};
export const Decorations = {
blameAnnotation: window.createTextEditorDecorationType({
isWholeLine: true,
textDecoration: 'none'
} as DecorationRenderOptions),
blameHighlight: undefined as TextEditorDecorationType | undefined,
recentChangesAnnotation: undefined as TextEditorDecorationType | undefined,
recentChangesHighlight: undefined as TextEditorDecorationType | undefined
};
export class AnnotationController extends Disposable {
private _onDidToggleAnnotations = new EventEmitter<void>();
get onDidToggleAnnotations(): Event<void> {
return this._onDidToggleAnnotations.event;
}
private _annotationsDisposable: Disposable | undefined;
private _annotationProviders: Map<number, AnnotationProviderBase> = 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.blameAnnotation && Decorations.blameAnnotation.dispose();
Decorations.blameHighlight && Decorations.blameHighlight.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<boolean>('enabled');
// Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
// TODO: detect monospace vs non-monospace font
// if (!toggleWhitespace) {
// // Since we know ligatures will break the whitespace rendering -- turn it back on
// toggleWhitespace = workspace.getConfiguration('editor').get<boolean>('fontLigatures', false);
// }
// If the setting is on and we aren't showing any annotations, make sure it is necessary (i.e. only when rendering whitespace)
if (toggleWhitespace && this._annotationProviders.size === 0) {
toggleWhitespace = (workspace.getConfiguration('editor').get<string>('renderWhitespace') !== 'none');
}
let changed = false;
if (toggleWhitespace && this._whitespaceController === undefined) {
changed = true;
this._whitespaceController = new WhitespaceController();
}
else if (!toggleWhitespace && this._whitespaceController !== undefined) {
changed = true;
this._whitespaceController.dispose();
this._whitespaceController = undefined;
}
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const cfgBlameHighlight = cfg.blame.file.lineHighlight;
const cfgChangesHighlight = cfg.recentChanges.file.lineHighlight;
const cfgTheme = cfg.theme.lineHighlight;
if (!Objects.areEquivalent(cfgBlameHighlight, this._config && this._config.blame.file.lineHighlight) ||
!Objects.areEquivalent(cfgChangesHighlight, this._config && this._config.recentChanges.file.lineHighlight) ||
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
changed = true;
Decorations.blameHighlight && Decorations.blameHighlight.dispose();
if (cfgBlameHighlight.enabled) {
Decorations.blameHighlight = window.createTextEditorDecorationType({
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
dark: {
backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
: undefined,
gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-dark.svg')
: undefined,
overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
: undefined
},
light: {
backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
: undefined,
gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-light.svg')
: undefined,
overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
: undefined
}
});
}
else {
Decorations.blameHighlight = undefined;
}
Decorations.recentChangesHighlight && Decorations.recentChangesHighlight.dispose();
Decorations.recentChangesHighlight = window.createTextEditorDecorationType({
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
dark: {
backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
: undefined,
gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-dark.svg')
: undefined,
overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
: undefined
},
light: {
backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
: undefined,
gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-light.svg')
: undefined,
overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
: undefined
}
});
}
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
!Objects.areEquivalent(cfg.recentChanges.file, this._config && this._config.recentChanges.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;
if (provider.annotationType === FileAnnotationType.RecentChanges) {
provider.reset(Decorations.recentChangesAnnotation, Decorations.recentChangesHighlight);
}
else {
provider.reset(Decorations.blameAnnotation, Decorations.blameHighlight, this._whitespaceController);
}
}
}
}
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<boolean> {
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.blameAnnotation, Decorations.blameHighlight, this._whitespaceController, this.git, gitUri);
break;
case FileAnnotationType.Hover:
provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this._whitespaceController, this.git, gitUri);
break;
case FileAnnotationType.RecentChanges:
provider = new RecentChangesAnnotationProvider(this.context, editor, undefined, Decorations.recentChangesHighlight!, 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<boolean> {
if (!editor || !editor.document || type === FileAnnotationType.RecentChanges ? !this.git.isTrackable(editor.document.uri) : !this.git.isEditorBlameable(editor)) return false;
const provider = this._annotationProviders.get(editor.viewColumn || -1);
if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine);
const reopen = provider.annotationType !== type;
await this.clear(provider.editor.viewColumn || -1);
if (!reopen) return false;
return this.showAnnotations(editor, type, shaOrLine);
}
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;
// TODO: Rework this once https://github.com/Microsoft/vscode/issues/27231 is released in v1.13
// 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);
}
}
}

View File

@@ -0,0 +1,82 @@
'use strict';
import { Functions } from '../system';
import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { FileAnnotationType } from '../annotations/annotationController';
import { TextDocumentComparer } from '../comparers';
import { ExtensionKey, 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 | undefined, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) {
super(() => this.dispose());
this.document = this.editor.document;
this._config = workspace.getConfiguration().get<IConfig>(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 {
if (this.decoration !== undefined) {
this.editor.setDecorations(this.decoration, []);
}
if (this.highlightDecoration !== undefined) {
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
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(decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController?: WhitespaceController) {
await this.clear();
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
this.decoration = decoration;
this.highlightDecoration = highlightDecoration;
this.whitespaceController = whitespaceController;
await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line);
}
abstract async provideAnnotation(shaOrLine?: string | number): Promise<boolean>;
abstract async selection(shaOrLine?: string | number): Promise<void>;
abstract async validate(): Promise<boolean>;
}

View File

@@ -0,0 +1,191 @@
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<DecorationOptions> {
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
}
},
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%',
margin: '0 26px 0 0',
textDecoration: 'none'
},
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
} as IRenderOptions;
}
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',
textDecoration: 'none'
}
} 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 } };
}
}

View File

@@ -0,0 +1,81 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { GitBlame, GitService, GitUri } from '../gitService';
import { WhitespaceController } from './whitespaceController';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
protected _blame: Promise<GitBlame>;
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, 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?: GitBlame) {
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<boolean> {
const blame = await this._blame;
return blame !== undefined && blame.lines.length !== 0;
}
protected async getBlame(requiresWhitespaceHack: boolean): Promise<GitBlame | undefined> {
let whitespacePromise: Promise<void> | 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: GitBlame;
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;
}
}

View File

@@ -0,0 +1,76 @@
'use strict';
import { Strings } from '../system';
import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { Annotations, endOfLineIndex } from './annotations';
import { ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise<boolean> {
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;
}
}

View File

@@ -0,0 +1,49 @@
'use strict';
import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { Annotations, endOfLineIndex } from './annotations';
import * as moment from 'moment';
export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
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;
}
}

View File

@@ -0,0 +1,61 @@
'use strict';
import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { endOfLineIndex } from './annotations';
import { FileAnnotationType } from './annotationController';
import { AnnotationProviderBase } from './annotationProvider';
import { CommitFormatter, GitService, GitUri } from '../gitService';
export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, private git: GitService, private uri: GitUri) {
super(context, editor, decoration, highlightDecoration, undefined);
}
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
this.annotationType = FileAnnotationType.RecentChanges;
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 cfg = this._config.annotations.file.recentChanges;
const decorators: DecorationOptions[] = [];
for (const chunk of diff.chunks) {
let count = chunk.currentPosition.start - 2;
for (const change of chunk.current) {
if (change === undefined) continue;
count++;
if (change.state === 'unchanged') continue;
let endingIndex = 0;
let message: string | undefined = undefined;
if (cfg.hover.changes) {
message = CommitFormatter.toHoverDiff(commit, chunk.previous[count], change);
endingIndex = cfg.hover.wholeLine ? endOfLineIndex : this.editor.document.lineAt(count).firstNonWhitespaceCharacterIndex;
}
decorators.push({
hoverMessage: message,
range: this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endingIndex)))
} as DecorationOptions);
}
}
this.editor.setDecorations(this.highlightDecoration!, decorators);
return true;
}
async selection(shaOrLine?: string | number): Promise<void> {
}
async validate(): Promise<boolean> {
return true;
}
}

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Disposable, workspace } from 'vscode'; import { Disposable, workspace } from 'vscode';
import { Logger } from './logger'; import { Logger } from '../logger';
interface ConfigurationInspection { interface ConfigurationInspection {
key: string; key: string;
@@ -118,8 +118,6 @@ export class WhitespaceController extends Disposable {
if (this._count === 1 && this._configuration.overrideRequired) { if (this._count === 1 && this._configuration.overrideRequired) {
// Override whitespace (turn off) // Override whitespace (turn off)
await this._overrideWhitespace(); await this._overrideWhitespace();
// Add a delay to give the editor time to turn off the whitespace
await new Promise((resolve, reject) => setTimeout(resolve, 250));
} }
} }

View File

@@ -1,390 +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 { 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<void>;
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<IConfig>(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._updateBlame(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._updateBlame(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<void> {
// 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.tooltip = 'Compare with Previous Commit';
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.repoPath, this._uri.fsPath, blameLine.line + offset, commit.previousSha);
// if (changes !== undefined) {
// const previous = changes[0];
// if (previous !== undefined) {
// hoverMessage += `\n\n\`Before ${commit.shortSha}\`\n\`\`\`\n${previous.trim().replace(/\n/g, '\`\n>\n> \`')}\n\`\`\``;
// }
// else {
// hoverMessage += `\n\n\`Added in ${commit.shortSha}\``;
// }
// }
// }
}
else if (commit.isUncommitted) {
const changes = await this.git.getDiffForLine(this._uri.repoPath, this._uri.fsPath, blameLine.line + offset);
if (changes !== undefined) {
let original = changes[0];
if (original !== undefined) {
original = original.replace(/\n/g, '\`\n>\n> \`').trim();
hoverMessage = `\`${'0'.repeat(8)}\` &nbsp; __Uncommitted change__\n\n\---\n\`\`\`\n${original}\n\`\`\``;
}
// else {
// hoverMessage = `\`${'0'.repeat(8)}\` &nbsp; __Uncommitted change__\n\n\`Added\``;
// }
}
}
}
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);
}
}
}
}

View File

@@ -1,253 +0,0 @@
'use strict';
import { Functions } from './system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, 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<void>();
get onDidToggleBlameAnnotations(): Event<void> {
return this._onDidToggleBlameAnnotations.event;
}
private _annotationProviders: Map<number, BlameAnnotationProvider> = 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<boolean>('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<boolean>('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<IBlameConfig>('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<boolean> {
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.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<boolean> {
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 _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);
}
}
}

View File

@@ -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)}\` &nbsp; __Uncommitted change__`;
}
return `\`${commit.shortSha}\` &nbsp; __${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;
}
}

View File

@@ -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<IGitBlame>;
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<IBlameConfig>('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<boolean> {
const blame = await this._blame;
return !!(blame && blame.lines.length);
}
async provideBlameAnnotation(shaOrLine?: string | number): Promise<boolean> {
let whitespacePromise: Promise<void> | 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;
});
}
}

View File

@@ -19,10 +19,12 @@ export * from './commands/openCommitInRemote';
export * from './commands/openFileInRemote'; export * from './commands/openFileInRemote';
export * from './commands/openInRemote'; export * from './commands/openInRemote';
export * from './commands/openRepoInRemote'; export * from './commands/openRepoInRemote';
export * from './commands/showBlame'; export * from './commands/resetSuppressedWarnings';
export * from './commands/showBlameHistory'; export * from './commands/showBlameHistory';
export * from './commands/showFileBlame';
export * from './commands/showFileHistory'; export * from './commands/showFileHistory';
export * from './commands/showLastQuickPick'; export * from './commands/showLastQuickPick';
export * from './commands/showLineBlame';
export * from './commands/showQuickCommitDetails'; export * from './commands/showQuickCommitDetails';
export * from './commands/showQuickCommitFileDetails'; export * from './commands/showQuickCommitFileDetails';
export * from './commands/showCommitSearch'; export * from './commands/showCommitSearch';
@@ -34,5 +36,7 @@ export * from './commands/showQuickStashList';
export * from './commands/stashApply'; export * from './commands/stashApply';
export * from './commands/stashDelete'; export * from './commands/stashDelete';
export * from './commands/stashSave'; export * from './commands/stashSave';
export * from './commands/toggleBlame'; export * from './commands/toggleCodeLens';
export * from './commands/toggleCodeLens'; export * from './commands/toggleFileBlame';
export * from './commands/toggleFileRecentChanges';
export * from './commands/toggleLineBlame';

View File

@@ -1,10 +1,12 @@
'use strict'; 'use strict';
import { TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorTracker } from '../activeEditorTracker'; import { ActiveEditorTracker } from '../activeEditorTracker';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { TextEditorComparer, UriComparer } from '../comparers'; import { TextEditorComparer, UriComparer } from '../comparers';
import { BuiltInCommands } from '../constants';
import { GitService } from '../gitService'; import { GitService } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
export interface CloseUnchangedFilesCommandArgs { export interface CloseUnchangedFilesCommandArgs {
uris?: Uri[]; uris?: Uri[];
@@ -22,7 +24,7 @@ export class CloseUnchangedFilesCommand extends ActiveEditorCommand {
try { try {
if (args.uris === undefined) { if (args.uris === undefined) {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to close unchanged files`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to close unchanged files`);
const status = await this.git.getStatusForRepo(repoPath); const status = await this.git.getStatusForRepo(repoPath);
if (status === undefined) return window.showWarningMessage(`Unable to close unchanged files`); if (status === undefined) return window.showWarningMessage(`Unable to close unchanged files`);
@@ -30,34 +32,41 @@ export class CloseUnchangedFilesCommand extends ActiveEditorCommand {
args.uris = status.files.map(_ => _.Uri); args.uris = status.files.map(_ => _.Uri);
} }
if (args.uris.length === 0) return commands.executeCommand(BuiltInCommands.CloseAllEditors);
const editorTracker = new ActiveEditorTracker(); const editorTracker = new ActiveEditorTracker();
let active = window.activeTextEditor; let count = 0;
let editor = active; let previous = undefined;
do { let editor = window.activeTextEditor;
while (true) {
if (editor !== undefined) { if (editor !== undefined) {
if ((editor.document !== undefined && editor.document.isDirty) || if (TextEditorComparer.equals(previous, editor, { useId: true, usePosition: true })) {
args.uris.some(_ => UriComparer.equals(_, editor!.document && editor!.document.uri))) { break;
// If we didn't start with a valid editor, set one once we find it
if (active === undefined) {
active = editor;
}
editor = await editorTracker.awaitNext(500);
} }
else {
if (active === editor) { if (editor.document !== undefined &&
active = undefined; (editor.document.isDirty || args.uris.some(_ => UriComparer.equals(_, editor!.document && editor!.document.uri)))) {
} previous = editor;
editor = await editorTracker.awaitClose(500); editor = await editorTracker.awaitNext(500);
continue;
}
}
previous = editor;
editor = await editorTracker.awaitClose(500);
if (previous === undefined && editor === undefined) {
count++;
// This is such a shitty hack, but I can't figure out any other reliable way to know that we've cycled through all the editors :(
if (count >= 4) {
break;
} }
} }
else { else {
if (active === editor) { count = 0;
active = undefined;
}
editor = await editorTracker.awaitClose(500);
} }
} while ((active === undefined && editor === undefined) || !TextEditorComparer.equals(active, editor, { useId: true, usePosition: true })); }
editorTracker.dispose(); editorTracker.dispose();

View File

@@ -4,16 +4,43 @@ import { BuiltInCommands } from '../constants';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Telemetry } from '../telemetry'; import { Telemetry } from '../telemetry';
export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | export type Commands = 'gitlens.closeUnchangedFiles' |
'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.copyMessageToClipboard' |
'gitlens.openChangedFiles' | 'gitlens.openBranchInRemote' | 'gitlens.openCommitInRemote' | 'gitlens.openFileInRemote' | 'gitlens.openInRemote' | 'gitlens.openRepoInRemote' | 'gitlens.copyShaToClipboard' |
'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileHistory' | 'gitlens.diffDirectory' |
'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' | 'gitlens.diffWithBranch' |
'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.diffWithNext' |
'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' | 'gitlens.diffWithPrevious' |
'gitlens.showQuickRepoStatus' | 'gitlens.showQuickStashList' | 'gitlens.diffLineWithPrevious' |
'gitlens.stashApply' | 'gitlens.stashDelete' | 'gitlens.stashSave' | 'gitlens.diffWithWorking' |
'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; 'gitlens.diffLineWithWorking' |
'gitlens.openChangedFiles' |
'gitlens.openBranchInRemote' |
'gitlens.openCommitInRemote' |
'gitlens.openFileInRemote' |
'gitlens.openInRemote' |
'gitlens.openRepoInRemote' |
'gitlens.resetSuppressedWarnings' |
'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.toggleCodeLens' |
'gitlens.toggleFileBlame' |
'gitlens.toggleFileRecentChanges' |
'gitlens.toggleLineBlame';
export const Commands = { export const Commands = {
CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands, CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands,
CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands, CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands,
@@ -31,11 +58,13 @@ export const Commands = {
OpenFileInRemote: 'gitlens.openFileInRemote' as Commands, OpenFileInRemote: 'gitlens.openFileInRemote' as Commands,
OpenInRemote: 'gitlens.openInRemote' as Commands, OpenInRemote: 'gitlens.openInRemote' as Commands,
OpenRepoInRemote: 'gitlens.openRepoInRemote' as Commands, OpenRepoInRemote: 'gitlens.openRepoInRemote' as Commands,
ShowBlame: 'gitlens.showBlame' as Commands, ResetSuppressedWarnings: 'gitlens.resetSuppressedWarnings' as Commands,
ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands,
ShowCommitSearch: 'gitlens.showCommitSearch' as Commands, ShowCommitSearch: 'gitlens.showCommitSearch' as Commands,
ShowFileBlame: 'gitlens.showFileBlame' as Commands,
ShowFileHistory: 'gitlens.showFileHistory' as Commands, ShowFileHistory: 'gitlens.showFileHistory' as Commands,
ShowLastQuickPick: 'gitlens.showLastQuickPick' as Commands, ShowLastQuickPick: 'gitlens.showLastQuickPick' as Commands,
ShowLineBlame: 'gitlens.showLineBlame' as Commands,
ShowQuickCommitDetails: 'gitlens.showQuickCommitDetails' as Commands, ShowQuickCommitDetails: 'gitlens.showQuickCommitDetails' as Commands,
ShowQuickCommitFileDetails: 'gitlens.showQuickCommitFileDetails' as Commands, ShowQuickCommitFileDetails: 'gitlens.showQuickCommitFileDetails' as Commands,
ShowQuickFileHistory: 'gitlens.showQuickFileHistory' as Commands, ShowQuickFileHistory: 'gitlens.showQuickFileHistory' as Commands,
@@ -46,8 +75,10 @@ export const Commands = {
StashApply: 'gitlens.stashApply' as Commands, StashApply: 'gitlens.stashApply' as Commands,
StashDelete: 'gitlens.stashDelete' as Commands, StashDelete: 'gitlens.stashDelete' as Commands,
StashSave: 'gitlens.stashSave' as Commands, StashSave: 'gitlens.stashSave' as Commands,
ToggleBlame: 'gitlens.toggleBlame' as Commands, ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands,
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands ToggleFileBlame: 'gitlens.toggleFileBlame' as Commands,
ToggleFileRecentChanges: 'gitlens.toggleFileRecentChanges' as Commands,
ToggleLineBlame: 'gitlens.toggleLineBlame' as Commands
}; };
export function getCommandUri(uri?: Uri, editor?: TextEditor): Uri | undefined { export function getCommandUri(uri?: Uri, editor?: TextEditor): Uri | undefined {
@@ -56,7 +87,13 @@ export function getCommandUri(uri?: Uri, editor?: TextEditor): Uri | undefined {
return editor.document.uri; return editor.document.uri;
} }
export type CommandContext = 'gitlens:canToggleCodeLens' | 'gitlens:enabled' | 'gitlens:hasRemotes' | 'gitlens:isBlameable' | 'gitlens:isRepository' | 'gitlens:isTracked' | 'gitlens:key'; export type CommandContext = 'gitlens:canToggleCodeLens' |
'gitlens:enabled' |
'gitlens:hasRemotes' |
'gitlens:isBlameable' |
'gitlens:isRepository' |
'gitlens:isTracked' |
'gitlens:key';
export const CommandContext = { export const CommandContext = {
CanToggleCodeLens: 'gitlens:canToggleCodeLens' as CommandContext, CanToggleCodeLens: 'gitlens:canToggleCodeLens' as CommandContext,
Enabled: 'gitlens:enabled' as CommandContext, Enabled: 'gitlens:enabled' as CommandContext,

View File

@@ -5,6 +5,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitService } from '../gitService'; import { GitService } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks'; import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
export interface DiffDirectoryCommandCommandArgs { export interface DiffDirectoryCommandCommandArgs {
@@ -31,7 +32,7 @@ export class DiffDirectoryCommand extends ActiveEditorCommand {
try { try {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to open directory compare`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open directory compare`);
if (!args.shaOrBranch1) { if (!args.shaOrBranch1) {
const branches = await this.git.getBranches(repoPath); const branches = await this.git.getBranches(repoPath);

View File

@@ -6,6 +6,7 @@ import { DiffWithPreviousCommandArgs } from './diffWithPrevious';
import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { DiffWithWorkingCommandArgs } from './diffWithWorking';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface DiffLineWithPreviousCommandArgs { export interface DiffLineWithPreviousCommandArgs {
@@ -35,7 +36,7 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand {
try { try {
const blame = await this.git.getBlameForLine(gitUri, blameline); const blame = await this.git.getBlameForLine(gitUri, blameline);
if (blame === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
args.commit = blame.commit; args.commit = blame.commit;

View File

@@ -3,6 +3,7 @@ import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vsco
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { DiffWithWorkingCommandArgs } from './diffWithWorking';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Messages } from '../messages';
import { Logger } from '../logger'; import { Logger } from '../logger';
export interface DiffLineWithWorkingCommandArgs { export interface DiffLineWithWorkingCommandArgs {
@@ -32,7 +33,7 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand {
try { try {
const blame = await this.git.getBlameForLine(gitUri, blameline); const blame = await this.git.getBlameForLine(gitUri, blameline);
if (blame === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
args.commit = blame.commit; args.commit = blame.commit;
// If the line is uncommitted, find the previous commit // If the line is uncommitted, find the previous commit

View File

@@ -4,6 +4,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks'; import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
import * as path from 'path'; import * as path from 'path';
@@ -27,7 +28,7 @@ export class DiffWithBranchCommand extends ActiveEditorCommand {
args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line); args.line = args.line || (editor === undefined ? 0 : editor.selection.active.line);
const gitUri = await GitUri.fromUri(uri, this.git); const gitUri = await GitUri.fromUri(uri, this.git);
if (!gitUri.repoPath) return window.showWarningMessage(`Unable to open branch compare`); if (!gitUri.repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open branch compare`);
const branches = await this.git.getBranches(gitUri.repoPath); const branches = await this.git.getBranches(gitUri.repoPath);
const pick = await BranchesQuickPick.show(branches, `Compare ${path.basename(gitUri.fsPath)} to \u2026`, args.goBackCommand); const pick = await BranchesQuickPick.show(branches, `Compare ${path.basename(gitUri.fsPath)} to \u2026`, args.goBackCommand);

View File

@@ -5,6 +5,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitLogCommit, GitService, GitUri } from '../gitService'; import { GitLogCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface DiffWithNextCommandArgs { export interface DiffWithNextCommandArgs {
@@ -37,8 +38,8 @@ export class DiffWithNextCommand extends ActiveEditorCommand {
const sha = args.commit === undefined ? gitUri.sha : args.commit.sha; const sha = args.commit === undefined ? gitUri.sha : args.commit.sha;
const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, args.range!); const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha !== undefined ? undefined : 2, args.range!);
if (log === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); if (log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values());
} }

View File

@@ -6,7 +6,7 @@ import { BuiltInCommands } from '../constants';
import { DiffWithWorkingCommandArgs } from './diffWithWorking'; import { DiffWithWorkingCommandArgs } from './diffWithWorking';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import * as moment from 'moment'; import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface DiffWithPreviousCommandArgs { export interface DiffWithPreviousCommandArgs {
@@ -32,17 +32,15 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
const gitUri = await GitUri.fromUri(uri, this.git); const gitUri = await GitUri.fromUri(uri, this.git);
try { try {
// If the sha is missing or the file is uncommitted, treat it as a DiffWithWorking
if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) {
return commands.executeCommand(Commands.DiffWithWorking, uri, { showOptions: args.showOptions } as DiffWithWorkingCommandArgs);
}
const sha = args.commit === undefined ? gitUri.sha : args.commit.sha; const sha = args.commit === undefined ? gitUri.sha : args.commit.sha;
const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, args.range!); const log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha !== undefined ? undefined : 2, args.range!);
if (log === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); if (log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values()); args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values());
// If the sha is missing and the file is uncommitted, then treat it as a DiffWithWorking
if (gitUri.sha === undefined && await this.git.isFileUncommitted(gitUri)) return commands.executeCommand(Commands.DiffWithWorking, uri, { commit: args.commit, showOptions: args.showOptions } as DiffWithWorkingCommandArgs);
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`); Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`);
@@ -50,7 +48,7 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
} }
} }
if (args.commit.previousSha === undefined) return window.showInformationMessage(`Commit ${args.commit.shortSha} (${args.commit.author}, ${moment(args.commit.date).fromNow()}) has no previous commit`); if (args.commit.previousSha === undefined) return Messages.showCommitHasNoPreviousCommitWarningMessage(args.commit);
try { try {
const [rhs, lhs] = await Promise.all([ const [rhs, lhs] = await Promise.all([

View File

@@ -4,6 +4,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface DiffWithWorkingCommandArgs { export interface DiffWithWorkingCommandArgs {
@@ -26,10 +27,12 @@ export class DiffWithWorkingCommand extends ActiveEditorCommand {
if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) {
const gitUri = await GitUri.fromUri(uri, this.git); const gitUri = await GitUri.fromUri(uri, this.git);
// If the sha is missing, just let the user know the file matches
if (gitUri.sha === undefined) return window.showInformationMessage(`File matches the working tree`);
try { try {
args.commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true }); args.commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true });
if (args.commit === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`); if (args.commit === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare');
} }
catch (ex) { catch (ex) {
Logger.error(ex, 'DiffWithWorkingCommand', `getLogCommit(${gitUri.repoPath}, ${gitUri.fsPath}, ${gitUri.sha})`); Logger.error(ex, 'DiffWithWorkingCommand', `getLogCommit(${gitUri.repoPath}, ${gitUri.fsPath}, ${gitUri.sha})`);

View File

@@ -3,6 +3,7 @@ import { TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri, openEditor } from './common'; import { ActiveEditorCommand, Commands, getCommandUri, openEditor } from './common';
import { GitService } from '../gitService'; import { GitService } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
export interface OpenChangedFilesCommandArgs { export interface OpenChangedFilesCommandArgs {
uris?: Uri[]; uris?: Uri[];
@@ -20,7 +21,7 @@ export class OpenChangedFilesCommand extends ActiveEditorCommand {
try { try {
if (args.uris === undefined) { if (args.uris === undefined) {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to open changed files`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open changed files`);
const status = await this.git.getStatusForRepo(repoPath); const status = await this.git.getStatusForRepo(repoPath);
if (status === undefined) return window.showWarningMessage(`Unable to open changed files`); if (status === undefined) return window.showWarningMessage(`Unable to open changed files`);

View File

@@ -2,8 +2,9 @@
import { Arrays } from '../system'; import { Arrays } from '../system';
import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { ActiveEditorCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { GitCommit, GitService, GitUri } from '../gitService'; import { GitBlameCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { OpenInRemoteCommandArgs } from './openInRemote'; import { OpenInRemoteCommandArgs } from './openInRemote';
export class OpenCommitInRemoteCommand extends ActiveEditorCommand { export class OpenCommitInRemoteCommand extends ActiveEditorCommand {
@@ -27,12 +28,12 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand {
if (blameline < 0) return undefined; if (blameline < 0) return undefined;
const blame = await this.git.getBlameForLine(gitUri, blameline); const blame = await this.git.getBlameForLine(gitUri, blameline);
if (blame === undefined) return window.showWarningMessage(`Unable to open commit in remote provider. File is probably not under source control`); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open commit in remote provider');
let commit = blame.commit; let commit = blame.commit;
// If the line is uncommitted, find the previous commit // If the line is uncommitted, find the previous commit
if (commit.isUncommitted) { if (commit.isUncommitted) {
commit = new GitCommit(commit.type, commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message); commit = new GitBlameCommit(commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message, []);
} }
const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider);

View File

@@ -0,0 +1,18 @@
'use strict';
import { Objects } from '../system';
import { ExtensionContext } from 'vscode';
import { Command, Commands } from './common';
import { SuppressedKeys } from '../messages';
export class ResetSuppressedWarningsCommand extends Command {
constructor(private context: ExtensionContext) {
super(Commands.ResetSuppressedWarnings);
}
async execute() {
for (const key of Objects.values<string>(SuppressedKeys)) {
await this.context.globalState.update(key, false);
}
}
}

View File

@@ -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<any> {
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`);
}
}
}

View File

@@ -3,6 +3,7 @@ import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } fr
import { Commands, EditorCommand, getCommandUri } from './common'; import { Commands, EditorCommand, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Messages } from '../messages';
import { Logger } from '../logger'; import { Logger } from '../logger';
export interface ShowBlameHistoryCommandArgs { export interface ShowBlameHistoryCommandArgs {
@@ -32,7 +33,7 @@ export class ShowBlameHistoryCommand extends EditorCommand {
try { try {
const locations = await this.git.getBlameLocations(gitUri, args.range, args.sha, args.line); const locations = await this.git.getBlameLocations(gitUri, args.range, args.sha, args.line);
if (locations === undefined) return window.showWarningMessage(`Unable to show blame history. File is probably not under source control`); if (locations === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show blame history');
return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations); return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations);
} }

View File

@@ -3,6 +3,7 @@ import { commands, InputBoxOptions, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitRepoSearchBy, GitService, GitUri } from '../gitService'; import { GitRepoSearchBy, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, CommitsQuickPick } from '../quickPicks'; import { CommandQuickPickItem, CommitsQuickPick } from '../quickPicks';
import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails';
import { paste } from 'copy-paste'; import { paste } from 'copy-paste';
@@ -33,7 +34,7 @@ export class ShowCommitSearchCommand extends ActiveEditorCachedCommand {
const gitUri = uri === undefined ? undefined : await GitUri.fromUri(uri, this.git); const gitUri = uri === undefined ? undefined : await GitUri.fromUri(uri, this.git);
const repoPath = gitUri === undefined ? this.git.repoPath : gitUri.repoPath; const repoPath = gitUri === undefined ? this.git.repoPath : gitUri.repoPath;
if (!repoPath) return window.showWarningMessage(`Unable to show commit search`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show commit search`);
if (!args.search || args.searchBy == null) { if (!args.search || args.searchBy == null) {
try { try {

View File

@@ -0,0 +1,35 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController, FileAnnotationType } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, 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<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
if (args.type === undefined) {
const cfg = workspace.getConfiguration().get<IConfig>(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`);
}
}
}

View File

@@ -3,6 +3,7 @@ import { commands, Position, Range, TextEditor, TextEditorEdit, Uri, window } fr
import { Commands, EditorCommand, getCommandUri } from './common'; import { Commands, EditorCommand, getCommandUri } from './common';
import { BuiltInCommands } from '../constants'; import { BuiltInCommands } from '../constants';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Messages } from '../messages';
import { Logger } from '../logger'; import { Logger } from '../logger';
export interface ShowFileHistoryCommandArgs { export interface ShowFileHistoryCommandArgs {
@@ -30,7 +31,7 @@ export class ShowFileHistoryCommand extends EditorCommand {
try { try {
const locations = await this.git.getLogLocations(gitUri, args.sha, args.line); const locations = await this.git.getLogLocations(gitUri, args.sha, args.line);
if (locations === undefined) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); if (locations === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show file history');
return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations); return commands.executeCommand(BuiltInCommands.ShowReferences, uri, args.position, locations);
} }

View File

@@ -0,0 +1,34 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { CurrentLineController, LineAnnotationType } from '../currentLineController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, IConfig } 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<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
if (args.type === undefined) {
const cfg = workspace.getConfiguration().get<IConfig>(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`);
}
}
}

View File

@@ -1,14 +1,15 @@
'use strict'; 'use strict';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitService, GitUri, IGitLog } from '../gitService'; import { GitLog, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { BranchesQuickPick, BranchHistoryQuickPick, CommandQuickPickItem } from '../quickPicks'; import { BranchesQuickPick, BranchHistoryQuickPick, CommandQuickPickItem } from '../quickPicks';
import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails';
export interface ShowQuickBranchHistoryCommandArgs { export interface ShowQuickBranchHistoryCommandArgs {
branch?: string; branch?: string;
log?: IGitLog; log?: GitLog;
maxCount?: number; maxCount?: number;
goBackCommand?: CommandQuickPickItem; goBackCommand?: CommandQuickPickItem;
@@ -33,7 +34,7 @@ export class ShowQuickBranchHistoryCommand extends ActiveEditorCachedCommand {
let progressCancellation = args.branch === undefined ? undefined : BranchHistoryQuickPick.showProgress(args.branch); let progressCancellation = args.branch === undefined ? undefined : BranchHistoryQuickPick.showProgress(args.branch);
try { try {
const repoPath = gitUri === undefined ? this.git.repoPath : gitUri.repoPath; const repoPath = gitUri === undefined ? this.git.repoPath : gitUri.repoPath;
if (!repoPath) return window.showWarningMessage(`Unable to show branch history`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show branch history`);
if (args.branch === undefined) { if (args.branch === undefined) {
const branches = await this.git.getBranches(repoPath); const branches = await this.git.getBranches(repoPath);

View File

@@ -1,16 +1,17 @@
'use strict'; 'use strict';
import { commands, TextEditor, Uri, window } from 'vscode'; import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitCommit, GitLogCommit, GitService, GitUri, IGitLog } from '../gitService'; import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { CommandQuickPickItem, CommitDetailsQuickPick, CommitWithFileStatusQuickPickItem } from '../quickPicks'; import { CommandQuickPickItem, CommitDetailsQuickPick, CommitWithFileStatusQuickPickItem } from '../quickPicks';
import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface ShowQuickCommitDetailsCommandArgs { export interface ShowQuickCommitDetailsCommandArgs {
sha?: string; sha?: string;
commit?: GitCommit | GitLogCommit; commit?: GitCommit | GitLogCommit;
repoLog?: IGitLog; repoLog?: GitLog;
goBackCommand?: CommandQuickPickItem; goBackCommand?: CommandQuickPickItem;
} }
@@ -38,9 +39,12 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
try { try {
const blame = await this.git.getBlameForLine(gitUri, blameline); const blame = await this.git.getBlameForLine(gitUri, blameline);
if (blame === undefined) return window.showWarningMessage(`Unable to show commit details. File is probably not under source control`); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show commit details');
args.sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; // Because the previous sha of an uncommitted file isn't trust worthy we just have to kick out
if (blame.commit.isUncommitted) return Messages.showLineUncommittedWarningMessage('Unable to show commit details');
args.sha = blame.commit.sha;
repoPath = blame.commit.repoPath; repoPath = blame.commit.repoPath;
workingFileName = blame.commit.fileName; workingFileName = blame.commit.fileName;
@@ -64,13 +68,13 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
if (args.repoLog === undefined) { if (args.repoLog === undefined) {
const log = await this.git.getLogForRepo(repoPath!, args.sha, 2); const log = await this.git.getLogForRepo(repoPath!, args.sha, 2);
if (log === undefined) return window.showWarningMessage(`Unable to show commit details`); if (log === undefined) return Messages.showCommitNotFoundWarningMessage(`Unable to show commit details`);
args.commit = log.commits.get(args.sha!); args.commit = log.commits.get(args.sha!);
} }
} }
if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit details`); if (args.commit === undefined) return Messages.showCommitNotFoundWarningMessage(`Unable to show commit details`);
if (args.commit.workingFileName === undefined) { if (args.commit.workingFileName === undefined) {
args.commit.workingFileName = workingFileName; args.commit.workingFileName = workingFileName;

View File

@@ -1,16 +1,17 @@
'use strict'; 'use strict';
import { TextEditor, Uri, window } from 'vscode'; import { TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitCommit, GitLogCommit, GitService, GitUri, IGitLog } from '../gitService'; import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { CommandQuickPickItem, CommitFileDetailsQuickPick } from '../quickPicks'; import { CommandQuickPickItem, CommitFileDetailsQuickPick } from '../quickPicks';
import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface ShowQuickCommitFileDetailsCommandArgs { export interface ShowQuickCommitFileDetailsCommandArgs {
sha?: string; sha?: string;
commit?: GitCommit | GitLogCommit; commit?: GitCommit | GitLogCommit;
fileLog?: IGitLog; fileLog?: GitLog;
goBackCommand?: CommandQuickPickItem; goBackCommand?: CommandQuickPickItem;
} }
@@ -37,9 +38,12 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand
try { try {
const blame = await this.git.getBlameForLine(gitUri, blameline); const blame = await this.git.getBlameForLine(gitUri, blameline);
if (blame === undefined) return window.showWarningMessage(`Unable to show commit file details. File is probably not under source control`); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show commit file details');
args.sha = blame.commit.isUncommitted ? blame.commit.previousSha : blame.commit.sha; // Because the previous sha of an uncommitted file isn't trust worthy we just have to kick out
if (blame.commit.isUncommitted) return Messages.showLineUncommittedWarningMessage('Unable to show commit file details');
args.sha = blame.commit.sha;
args.commit = blame.commit; args.commit = blame.commit;
workingFileName = path.relative(args.commit.repoPath, gitUri.fsPath); workingFileName = path.relative(args.commit.repoPath, gitUri.fsPath);
@@ -65,12 +69,12 @@ export class ShowQuickCommitFileDetailsCommand extends ActiveEditorCachedCommand
} }
if (args.fileLog === undefined) { if (args.fileLog === undefined) {
args.commit = await this.git.getLogCommit(args.commit ? args.commit.repoPath : gitUri.repoPath, gitUri.fsPath, args.sha, { previous: true }); args.commit = await this.git.getLogCommit(args.commit === undefined ? gitUri.repoPath : args.commit.repoPath, gitUri.fsPath, args.sha, { previous: true });
if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); if (args.commit === undefined) return Messages.showCommitNotFoundWarningMessage(`Unable to show commit file details`);
} }
} }
if (args.commit === undefined) return window.showWarningMessage(`Unable to show commit file details`); if (args.commit === undefined) return Messages.showCommitNotFoundWarningMessage(`Unable to show commit file details`);
// Attempt to the most recent commit -- so that we can find the real working filename if there was a rename // Attempt to the most recent commit -- so that we can find the real working filename if there was a rename
args.commit.workingFileName = workingFileName; args.commit.workingFileName = workingFileName;

View File

@@ -4,6 +4,7 @@ import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { ShowQuickBranchHistoryCommandArgs } from './showQuickBranchHistory'; import { ShowQuickBranchHistoryCommandArgs } from './showQuickBranchHistory';
import { GitService } from '../gitService'; import { GitService } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem } from '../quickPicks'; import { CommandQuickPickItem } from '../quickPicks';
export interface ShowQuickCurrentBranchHistoryCommandArgs { export interface ShowQuickCurrentBranchHistoryCommandArgs {
@@ -21,7 +22,7 @@ export class ShowQuickCurrentBranchHistoryCommand extends ActiveEditorCachedComm
try { try {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to show branch history`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show branch history`);
const branch = await this.git.getBranch(repoPath); const branch = await this.git.getBranch(repoPath);
if (branch === undefined) return undefined; if (branch === undefined) return undefined;

View File

@@ -1,14 +1,15 @@
'use strict'; 'use strict';
import { commands, Range, TextEditor, Uri, window } from 'vscode'; import { commands, Range, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitService, GitUri, IGitLog } from '../gitService'; import { GitLog, GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks';
import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails'; import { ShowQuickCommitFileDetailsCommandArgs } from './showQuickCommitFileDetails';
import { Messages } from '../messages';
import * as path from 'path'; import * as path from 'path';
export interface ShowQuickFileHistoryCommandArgs { export interface ShowQuickFileHistoryCommandArgs {
log?: IGitLog; log?: GitLog;
maxCount?: number; maxCount?: number;
range?: Range; range?: Range;
@@ -36,7 +37,7 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand {
try { try {
if (args.log === undefined) { if (args.log === undefined) {
args.log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, args.maxCount, args.range); args.log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, gitUri.sha, args.maxCount, args.range);
if (args.log === undefined) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); if (args.log === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to show file history');
} }
if (progressCancellation.token.isCancellationRequested) return undefined; if (progressCancellation.token.isCancellationRequested) return undefined;

View File

@@ -3,6 +3,7 @@ import { TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitService } from '../gitService'; import { GitService } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, RepoStatusQuickPick } from '../quickPicks'; import { CommandQuickPickItem, RepoStatusQuickPick } from '../quickPicks';
export interface ShowQuickRepoStatusCommandArgs { export interface ShowQuickRepoStatusCommandArgs {
@@ -20,7 +21,7 @@ export class ShowQuickRepoStatusCommand extends ActiveEditorCachedCommand {
try { try {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to show repository status`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show repository status`);
const status = await this.git.getStatusForRepo(repoPath); const status = await this.git.getStatusForRepo(repoPath);
if (status === undefined) return window.showWarningMessage(`Unable to show repository status`); if (status === undefined) return window.showWarningMessage(`Unable to show repository status`);

View File

@@ -3,6 +3,7 @@ import { commands, TextEditor, Uri, window } from 'vscode';
import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common'; import { ActiveEditorCachedCommand, Commands, getCommandUri } from './common';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, StashListQuickPick } from '../quickPicks'; import { CommandQuickPickItem, StashListQuickPick } from '../quickPicks';
import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails'; import { ShowQuickCommitDetailsCommandArgs } from './showQuickCommitDetails';
@@ -21,7 +22,7 @@ export class ShowQuickStashListCommand extends ActiveEditorCachedCommand {
try { try {
const repoPath = await this.git.getRepoPathFromUri(uri); const repoPath = await this.git.getRepoPathFromUri(uri);
if (!repoPath) return window.showWarningMessage(`Unable to show stashed changes`); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show stashed changes`);
const stash = await this.git.getStashList(repoPath); const stash = await this.git.getStashList(repoPath);
if (stash === undefined) return window.showWarningMessage(`Unable to show stashed changes`); if (stash === undefined) return window.showWarningMessage(`Unable to show stashed changes`);

View File

@@ -21,7 +21,6 @@ export class StashApplyCommand extends Command {
} }
async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) { async execute(args: StashApplyCommandArgs = { confirm: true, deleteAfter: false }) {
if (!this.git.config.insiders) return undefined;
if (!this.git.repoPath) return undefined; if (!this.git.repoPath) return undefined;
if (args.stashItem === undefined || args.stashItem.stashName === undefined) { if (args.stashItem === undefined || args.stashItem.stashName === undefined) {

View File

@@ -19,7 +19,6 @@ export class StashDeleteCommand extends Command {
} }
async execute(args: StashDeleteCommandArgs = { confirm: true }) { async execute(args: StashDeleteCommandArgs = { confirm: true }) {
if (!this.git.config.insiders) return undefined;
if (!this.git.repoPath) return undefined; if (!this.git.repoPath) return undefined;
if (args.stashItem === undefined || args.stashItem.stashName === undefined) return undefined; if (args.stashItem === undefined || args.stashItem.stashName === undefined) return undefined;

View File

@@ -19,7 +19,6 @@ export class StashSaveCommand extends Command {
} }
async execute(args: StashSaveCommandArgs = { unstagedOnly : false }) { async execute(args: StashSaveCommandArgs = { unstagedOnly : false }) {
if (!this.git.config.insiders) return undefined;
if (!this.git.repoPath) return undefined; if (!this.git.repoPath) return undefined;
if (args.unstagedOnly === undefined) { if (args.unstagedOnly === undefined) {

View File

@@ -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<any> {
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`);
}
}
}

View File

@@ -0,0 +1,35 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController, FileAnnotationType } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, 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<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
if (args.type === undefined) {
const cfg = workspace.getConfiguration().get<IConfig>(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`);
}
}
}

View File

@@ -0,0 +1,24 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { AnnotationController, FileAnnotationType } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common';
import { Logger } from '../logger';
export class ToggleFileRecentChangesCommand extends EditorCommand {
constructor(private annotationController: AnnotationController) {
super(Commands.ToggleFileRecentChanges);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
return this.annotationController.toggleAnnotations(editor, FileAnnotationType.RecentChanges);
}
catch (ex) {
Logger.error(ex, 'ToggleFileRecentChangesCommand');
return window.showErrorMessage(`Unable to toggle recent file changes annotations. See output channel for more details`);
}
}
}

View File

@@ -0,0 +1,34 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { CurrentLineController, LineAnnotationType } from '../currentLineController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, IConfig } 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<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
if (args.type === undefined) {
const cfg = workspace.getConfiguration().get<IConfig>(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`);
}
}
}

View File

@@ -1,32 +1,21 @@
'use strict'; 'use strict';
import { FileAnnotationType } from './annotations/annotationController';
import { Commands } from './commands'; import { Commands } from './commands';
import { LineAnnotationType } from './currentLineController';
import { OutputLevel } from './logger'; import { OutputLevel } from './logger';
export type BlameAnnotationStyle = 'compact' | 'expanded' | 'trailing'; export { ExtensionKey } from './constants';
export const BlameAnnotationStyle = {
Compact: 'compact' as BlameAnnotationStyle,
Expanded: 'expanded' as BlameAnnotationStyle,
Trailing: 'trailing' as BlameAnnotationStyle
};
export interface IBlameConfig { export type CodeLensCommand = 'gitlens.toggleFileBlame' |
annotation: { 'gitlens.showBlameHistory' |
style: BlameAnnotationStyle; 'gitlens.showFileHistory' |
highlight: 'none' | 'gutter' | 'line' | 'both'; 'gitlens.diffWithPrevious' |
sha: boolean; 'gitlens.showQuickCommitDetails' |
author: boolean; 'gitlens.showQuickCommitFileDetails' |
date: 'off' | 'relative' | 'absolute'; 'gitlens.showQuickFileHistory' |
dateFormat: string; 'gitlens.showQuickRepoHistory';
message: boolean;
activeLine: 'off' | 'inline' | 'hover' | 'both';
activeLineDarkColor: string;
activeLineLightColor: string;
};
}
export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export const CodeLensCommand = { export const CodeLensCommand = {
BlameAnnotate: Commands.ToggleBlame as CodeLensCommand, BlameAnnotate: Commands.ToggleFileBlame as CodeLensCommand,
ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand, ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand,
ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand, DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand,
@@ -36,49 +25,37 @@ export const CodeLensCommand = {
ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as CodeLensCommand ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as CodeLensCommand
}; };
export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom' | 'none'; export type CodeLensLocations = 'document' | 'containers' | 'blocks' | 'custom';
export const CodeLensLocation = { export const CodeLensLocations = {
All: 'all' as CodeLensLocation, Document: 'document' as CodeLensLocations,
DocumentAndContainers: 'document+containers' as CodeLensLocation, Containers: 'containers' as CodeLensLocations,
Document: 'document' as CodeLensLocation, Blocks: 'blocks' as CodeLensLocations,
Custom: 'custom' as CodeLensLocation, Custom: 'custom' as CodeLensLocations
None: 'none' as CodeLensLocation
}; };
export type CodeLensVisibility = 'auto' | 'ondemand' | 'off'; export type LineHighlightLocations = 'gutter' | 'line' | 'overviewRuler';
export const CodeLensVisibility = { export const LineHighlightLocations = {
Auto: 'auto' as CodeLensVisibility, Gutter: 'gutter' as LineHighlightLocations,
OnDemand: 'ondemand' as CodeLensVisibility, Line: 'line' as LineHighlightLocations,
Off: 'off' as CodeLensVisibility OverviewRuler: 'overviewRuler' as LineHighlightLocations
}; };
export interface ICodeLensConfig { export type StatusBarCommand = 'gitlens.toggleFileBlame' |
enabled: boolean; 'gitlens.showBlameHistory' |
command: CodeLensCommand; 'gitlens.showFileHistory' |
} 'gitlens.toggleCodeLens' |
'gitlens.diffWithPrevious' |
export interface ICodeLensLanguageLocation { 'gitlens.diffWithWorking' |
language: string | undefined; 'gitlens.showQuickCommitDetails' |
location: CodeLensLocation; 'gitlens.showQuickCommitFileDetails' |
customSymbols?: string[]; 'gitlens.showQuickFileHistory' |
} 'gitlens.showQuickRepoHistory';
export interface ICodeLensesConfig {
debug: boolean;
visibility: CodeLensVisibility;
location: CodeLensLocation;
locationCustomSymbols: string[];
languageLocations: ICodeLensLanguageLocation[];
recentChange: ICodeLensConfig;
authors: ICodeLensConfig;
}
export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export const StatusBarCommand = { export const StatusBarCommand = {
BlameAnnotate: Commands.ToggleBlame as StatusBarCommand, BlameAnnotate: Commands.ToggleFileBlame as StatusBarCommand,
ShowBlameHistory: Commands.ShowBlameHistory as StatusBarCommand, ShowBlameHistory: Commands.ShowBlameHistory as StatusBarCommand,
ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand, ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand, DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand,
DiffWithWorking: Commands.DiffWithWorking as StatusBarCommand,
ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand, ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand,
ShowQuickCommitDetails: Commands.ShowQuickCommitDetails as StatusBarCommand, ShowQuickCommitDetails: Commands.ShowQuickCommitDetails as StatusBarCommand,
ShowQuickCommitFileDetails: Commands.ShowQuickCommitFileDetails as StatusBarCommand, ShowQuickCommitFileDetails: Commands.ShowQuickCommitFileDetails as StatusBarCommand,
@@ -86,40 +63,266 @@ export const StatusBarCommand = {
ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as 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 { export interface IAdvancedConfig {
caching: { caching: {
enabled: boolean; enabled: boolean;
statusBar: { maxLines: number;
maxLines: number;
}
}; };
git: string; git: string;
gitignore: { gitignore: {
enabled: boolean; enabled: boolean;
}; };
maxQuickHistory: number; 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: { quickPick: {
closeOnFocusOut: boolean; closeOnFocusOut: boolean;
}; };
telemetry: {
enabled: boolean;
};
toggleWhitespace: { toggleWhitespace: {
enabled: boolean; enabled: boolean;
}; };
} }
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 { 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;
};
recentChanges: {
hover: {
changes: 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: LineHighlightLocations[];
};
};
line: {
enabled: boolean;
annotationType: LineAnnotationType;
};
};
recentChanges: {
file: {
lineHighlight: {
locations: LineHighlightLocations[];
};
}
};
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;
};
strings: {
codeLens: {
unsavedChanges: {
recentChangeAndAuthors: string;
recentChangeOnly: string;
authorsOnly: string;
};
};
};
theme: IThemeConfig;
debug: boolean; debug: boolean;
outputLevel: OutputLevel;
blame: IBlameConfig;
codeLens: ICodeLensesConfig;
statusBar: IStatusBarConfig;
advanced: IAdvancedConfig;
insiders: boolean; insiders: boolean;
outputLevel: OutputLevel;
advanced: IAdvancedConfig;
} }

View File

@@ -1,39 +1,51 @@
'use strict'; 'use strict';
export const ExtensionId = 'gitlens'; export const ExtensionId = 'gitlens';
export const ExtensionKey = ExtensionId; export const ExtensionKey = ExtensionId;
export const ExtensionOutputChannelName = 'GitLens'; export const ExtensionOutputChannelName = 'GitLens';
export const QualifiedExtensionId = `eamodio.${ExtensionId}`; export const QualifiedExtensionId = `eamodio.${ExtensionId}`;
export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679'; export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679';
export type BuiltInCommands = 'cursorMove' | 'editor.action.showReferences' | 'editor.action.toggleRenderWhitespace' | 'editorScroll' | 'revealLine' | 'setContext' | 'vscode.diff' | 'vscode.executeDocumentSymbolProvider' | 'vscode.executeCodeLensProvider' | 'vscode.open' | 'vscode.previewHtml' | 'workbench.action.closeActiveEditor' | 'workbench.action.nextEditor'; export type BuiltInCommands = 'cursorMove' |
export const BuiltInCommands = { 'editor.action.showReferences' |
CloseActiveEditor: 'workbench.action.closeActiveEditor' as BuiltInCommands, 'editor.action.toggleRenderWhitespace' |
CursorMove: 'cursorMove' as BuiltInCommands, 'editorScroll' |
Diff: 'vscode.diff' as BuiltInCommands, 'revealLine' |
EditorScroll: 'editorScroll' as BuiltInCommands, 'setContext' |
ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as BuiltInCommands, 'vscode.diff' |
ExecuteCodeLensProvider: 'vscode.executeCodeLensProvider' as BuiltInCommands, 'vscode.executeDocumentSymbolProvider' |
Open: 'vscode.open' as BuiltInCommands, 'vscode.executeCodeLensProvider' |
NextEditor: 'workbench.action.nextEditor' as BuiltInCommands, 'vscode.open' |
PreviewHtml: 'vscode.previewHtml' as BuiltInCommands, 'vscode.previewHtml' |
RevealLine: 'revealLine' as BuiltInCommands, 'workbench.action.closeActiveEditor' |
SetContext: 'setContext' as BuiltInCommands, 'workbench.action.closeAllEditors' |
ShowReferences: 'editor.action.showReferences' as BuiltInCommands, 'workbench.action.nextEditor';
ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands export const BuiltInCommands = {
}; CloseActiveEditor: 'workbench.action.closeActiveEditor' as BuiltInCommands,
CloseAllEditors: 'workbench.action.closeAllEditors' as BuiltInCommands,
export type DocumentSchemes = 'file' | 'git' | 'gitlens-git'; CursorMove: 'cursorMove' as BuiltInCommands,
export const DocumentSchemes = { Diff: 'vscode.diff' as BuiltInCommands,
File: 'file' as DocumentSchemes, EditorScroll: 'editorScroll' as BuiltInCommands,
Git: 'git' as DocumentSchemes, ExecuteDocumentSymbolProvider: 'vscode.executeDocumentSymbolProvider' as BuiltInCommands,
GitLensGit: 'gitlens-git' as DocumentSchemes ExecuteCodeLensProvider: 'vscode.executeCodeLensProvider' as BuiltInCommands,
}; Open: 'vscode.open' as BuiltInCommands,
NextEditor: 'workbench.action.nextEditor' as BuiltInCommands,
export type WorkspaceState = 'repoPath' | 'suppressGitVersionWarning' | 'suppressUpdateNotice'; PreviewHtml: 'vscode.previewHtml' as BuiltInCommands,
export const WorkspaceState = { RevealLine: 'revealLine' as BuiltInCommands,
GitLensVersion: 'gitlensVersion' as WorkspaceState, SetContext: 'setContext' as BuiltInCommands,
SuppressGitVersionWarning: 'suppressGitVersionWarning' as WorkspaceState, ShowReferences: 'editor.action.showReferences' as BuiltInCommands,
SuppressUpdateNotice: 'suppressUpdateNotice' as WorkspaceState ToggleRenderWhitespace: 'editor.action.toggleRenderWhitespace' as BuiltInCommands
};
export type DocumentSchemes = 'file' | 'git' | 'gitlens-git';
export const DocumentSchemes = {
File: 'file' as DocumentSchemes,
Git: 'git' as DocumentSchemes,
GitLensGit: 'gitlens-git' as DocumentSchemes
};
export type WorkspaceState = 'gitlensVersion';
export const WorkspaceState = {
GitLensVersion: 'gitlensVersion' as WorkspaceState
}; };

View File

@@ -0,0 +1,461 @@
'use strict';
import { Functions, Objects } from './system';
import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { AnnotationController, FileAnnotationType } from './annotations/annotationController';
import { Annotations, endOfLineIndex } from './annotations/annotations';
import { Commands } from './commands';
import { TextEditorComparer } from './comparers';
import { IConfig, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri } from './gitService';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: {
margin: '0 0 0 3em',
textDecoration: 'none'
}
} as DecorationRenderOptions);
export type LineAnnotationType = 'trailing' | 'hover';
export const LineAnnotationType = {
Trailing: 'trailing' as LineAnnotationType,
Hover: 'hover' as LineAnnotationType
};
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 _isAnnotating: boolean = false;
private _statusBarItem: StatusBarItem | undefined;
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>;
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._clearAnnotations(this._editor, true);
this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
this._disposable && this._disposable.dispose();
}
private _onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(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;
this._clearAnnotations(this._editor);
}
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;
this._clearAnnotations(this._editor);
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<void> {
// 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 !== undefined) {
this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git);
}
this._clearAnnotations(e.textEditor);
this._updateBlameDebounced(line, e.textEditor);
}
private async _updateBlame(line: number, editor: TextEditor) {
line = line - this._uri.offset;
let commit: GitCommit | undefined = undefined;
let commitLine: GitCommitLine | 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, line);
}
else {
this.clear(editor);
}
}
async clear(editor: TextEditor | undefined) {
this._clearAnnotations(editor, true);
this._statusBarItem && this._statusBarItem.hide();
}
private async _clearAnnotations(editor: TextEditor | undefined, force: boolean = false) {
if (editor === undefined || (!this._isAnnotating && !force)) return;
editor.setDecorations(annotationDecoration, []);
this._isAnnotating = false;
if (!force) return;
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
await Functions.wait(1);
editor.setDecorations(annotationDecoration, []);
}
async show(commit: GitCommit, blameLine: GitCommitLine, editor: TextEditor, line: number) {
// 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, line);
}
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: GitCommitLine, editor: TextEditor, line?: number) {
const cfg = this._config.blame.line;
if (!cfg.enabled) return;
line = line === undefined ? blameLine.line + this._uri.offset : line;
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;
}
case FileAnnotationType.RecentChanges: {
const cfgChanges = this._config.annotations.file.recentChanges.hover;
if (cfgChanges.changes) {
if (cfgChanges.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showChanges = false;
}
else {
showChangesInStartingWhitespace = false;
}
}
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);
this._isAnnotating = true;
}
}
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();
}
}

View File

@@ -1,14 +1,14 @@
'use strict'; 'use strict';
import { Objects } from './system'; // import { Objects } from './system';
import { commands, ExtensionContext, extensions, languages, Uri, window, workspace } from 'vscode'; import { ExtensionContext, extensions, languages, window, workspace } from 'vscode';
import { BlameActiveLineController } from './blameActiveLineController'; import { AnnotationController } from './annotations/annotationController';
import { BlameAnnotationController } from './blameAnnotationController';
import { CommandContext, setCommandContext } from './commands'; import { CommandContext, setCommandContext } from './commands';
import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands';
import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands'; import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands';
import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands';
import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
import { ShowBlameCommand, ToggleBlameCommand } from './commands'; import { ResetSuppressedWarningsCommand } from './commands';
import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands';
import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands';
import { ShowLastQuickPickCommand } from './commands'; import { ShowLastQuickPickCommand } from './commands';
import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands'; import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands';
@@ -17,17 +17,20 @@ import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './command
import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands'; import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands';
import { ToggleCodeLensCommand } from './commands'; import { ToggleCodeLensCommand } from './commands';
import { Keyboard } from './commands'; import { Keyboard } from './commands';
import { IConfig } from './configuration'; import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration';
import { ApplicationInsightsKey, BuiltInCommands, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants'; import { ApplicationInsightsKey, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants';
import { CurrentLineController, LineAnnotationType } from './currentLineController';
import { GitContentProvider } from './gitContentProvider'; import { GitContentProvider } from './gitContentProvider';
import { GitContextTracker, GitService } from './gitService'; import { GitContextTracker, GitService } from './gitService';
import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
import { Logger } from './logger'; import { Logger } from './logger';
import { Messages, SuppressedKeys } from './messages';
import { Telemetry } from './telemetry'; import { Telemetry } from './telemetry';
// this method is called when your extension is activated // this method is called when your extension is activated
export async function activate(context: ExtensionContext) { export async function activate(context: ExtensionContext) {
Logger.configure(context); Logger.configure(context);
Messages.configure(context);
Telemetry.configure(ApplicationInsightsKey); Telemetry.configure(ApplicationInsightsKey);
const gitlens = extensions.getExtension(QualifiedExtensionId)!; const gitlens = extensions.getExtension(QualifiedExtensionId)!;
@@ -61,9 +64,12 @@ export async function activate(context: ExtensionContext) {
telemetryContext['git.version'] = gitVersion; telemetryContext['git.version'] = gitVersion;
Telemetry.setContext(telemetryContext); Telemetry.setContext(telemetryContext);
await migrateSettings(context);
notifyOnUnsupportedGitVersion(context, gitVersion); notifyOnUnsupportedGitVersion(context, gitVersion);
notifyOnNewGitLensVersion(context, gitlensVersion); notifyOnNewGitLensVersion(context, gitlensVersion);
await context.globalState.update(WorkspaceState.GitLensVersion, gitlensVersion);
const git = new GitService(context, repoPath); const git = new GitService(context, repoPath);
context.subscriptions.push(git); context.subscriptions.push(git);
@@ -74,11 +80,11 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git))); 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); context.subscriptions.push(annotationController);
const activeLineController = new BlameActiveLineController(context, git, gitContextTracker, annotationController); const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController);
context.subscriptions.push(activeLineController); context.subscriptions.push(currentLineController);
context.subscriptions.push(new Keyboard()); context.subscriptions.push(new Keyboard());
@@ -98,8 +104,12 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new OpenFileInRemoteCommand(git)); context.subscriptions.push(new OpenFileInRemoteCommand(git));
context.subscriptions.push(new OpenInRemoteCommand()); context.subscriptions.push(new OpenInRemoteCommand());
context.subscriptions.push(new OpenRepoInRemoteCommand(git)); context.subscriptions.push(new OpenRepoInRemoteCommand(git));
context.subscriptions.push(new ShowBlameCommand(annotationController)); context.subscriptions.push(new ShowFileBlameCommand(annotationController));
context.subscriptions.push(new ToggleBlameCommand(annotationController)); context.subscriptions.push(new ShowLineBlameCommand(currentLineController));
context.subscriptions.push(new ToggleFileBlameCommand(annotationController));
context.subscriptions.push(new ToggleFileRecentChangesCommand(annotationController));
context.subscriptions.push(new ToggleLineBlameCommand(currentLineController));
context.subscriptions.push(new ResetSuppressedWarningsCommand(context));
context.subscriptions.push(new ShowBlameHistoryCommand(git)); context.subscriptions.push(new ShowBlameHistoryCommand(git));
context.subscriptions.push(new ShowFileHistoryCommand(git)); context.subscriptions.push(new ShowFileHistoryCommand(git));
context.subscriptions.push(new ShowLastQuickPickCommand()); context.subscriptions.push(new ShowLastQuickPickCommand());
@@ -116,42 +126,157 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new StashSaveCommand(git)); context.subscriptions.push(new StashSaveCommand(git));
context.subscriptions.push(new ToggleCodeLensCommand(git)); context.subscriptions.push(new ToggleCodeLensCommand(git));
Telemetry.trackEvent('initialized', Objects.flatten(cfg, 'config', true)); // Constantly over my data cap so stop collecting initialized event
// Telemetry.trackEvent('initialized', Objects.flatten(cfg, 'config', true));
} }
// this method is called when your extension is deactivated // this method is called when your extension is deactivated
export function deactivate() { } export function deactivate() { }
async function notifyOnNewGitLensVersion(context: ExtensionContext, version: string) { async function migrateSettings(context: ExtensionContext) {
if (context.globalState.get(WorkspaceState.SuppressUpdateNotice, false)) return;
const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion); const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion);
if (previousVersion === undefined) return;
await context.globalState.update(WorkspaceState.GitLensVersion, version); const [major] = previousVersion.split('.');
if (parseInt(major, 10) >= 4) return;
if (previousVersion) { try {
const [major, minor] = version.split('.'); const cfg = workspace.getConfiguration(ExtensionKey);
const [prevMajor, prevMinor] = previousVersion.split('.'); const prevCfg = workspace.getConfiguration().get<any>(ExtensionKey)!;
if (major === prevMajor && minor === prevMinor) return;
if (prevCfg.blame !== undefined && prevCfg.blame.annotation !== undefined) {
switch (prevCfg.blame.annotation.activeLine) {
case 'off':
await cfg.update('blame.line.enabled', false, true);
break;
case 'hover':
await cfg.update('blame.line.annotationType', LineAnnotationType.Hover, true);
break;
}
if (prevCfg.blame.annotation.activeLineDarkColor != null) {
await cfg.update('theme.annotations.line.trailing.dark.foregroundColor', prevCfg.blame.annotation.activeLineDarkColor, true);
}
if (prevCfg.blame.annotation.activeLineLightColor != null) {
await cfg.update('theme.annotations.line.trailing.light.foregroundColor', prevCfg.blame.annotation.activeLineLightColor, true);
}
switch (prevCfg.blame.annotation.highlight) {
case 'none':
await cfg.update('blame.file.lineHighlight.enabled', false);
break;
case 'gutter':
await cfg.update('blame.file.lineHighlight.locations', [LineHighlightLocations.Gutter, LineHighlightLocations.OverviewRuler], true);
break;
case 'line':
await cfg.update('blame.file.lineHighlight.locations', [LineHighlightLocations.Line, LineHighlightLocations.OverviewRuler], true);
break;
case 'both':
}
if (prevCfg.blame.annotation.dateFormat != null) {
await cfg.update('annotations.file.gutter.dateFormat', prevCfg.blame.annotation.dateFormat, true);
await cfg.update('annotations.line.trailing.dateFormat', prevCfg.blame.annotation.dateFormat, true);
}
}
if (prevCfg.codeLens !== undefined) {
switch (prevCfg.codeLens.visibility) {
case 'ondemand':
case 'off':
await cfg.update('codeLens.enabled', false);
}
switch (prevCfg.codeLens.location) {
case 'all':
await cfg.update('codeLens.locations', [CodeLensLocations.Document, CodeLensLocations.Containers, CodeLensLocations.Blocks], true);
break;
case 'document+containers':
await cfg.update('codeLens.locations', [CodeLensLocations.Document, CodeLensLocations.Containers], true);
break;
case 'document':
await cfg.update('codeLens.locations', [CodeLensLocations.Document], true);
break;
case 'custom':
await cfg.update('codeLens.locations', [CodeLensLocations.Custom], true);
break;
}
if (prevCfg.codeLens.locationCustomSymbols != null) {
await cfg.update('codeLens.customLocationSymbols', prevCfg.codeLens.locationCustomSymbols, true);
}
}
if ((prevCfg.menus && prevCfg.menus.diff && prevCfg.menus.diff.enabled) === false) {
await cfg.update('advanced.menus', {
editorContext: {
blame: true,
copy: true,
details: true,
fileDiff: false,
history: true,
lineDiff: false,
remote: true
},
editorTitle: {
blame: true,
fileDiff: false,
history: true,
remote: true,
status: true
},
editorTitleContext: {
blame: true,
fileDiff: false,
history: true,
remote: true
},
explorerContext: {
fileDiff: false,
history: true,
remote: true
}
}, true);
}
switch (prevCfg.statusBar && prevCfg.statusBar.date) {
case 'off':
await cfg.update('statusBar.format', '${author}', true);
break;
case 'absolute':
await cfg.update('statusBar.format', '${author}, ${date}', true);
break;
}
} }
catch (ex) {
const result = await window.showInformationMessage(`GitLens has been updated to v${version}`, 'View Release Notes', `Don't Show Again`); Logger.error(ex, 'migrateSettings');
if (result === 'View Release Notes') {
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens/changelog'));
} }
else if (result === `Don't Show Again`) { finally {
context.globalState.update(WorkspaceState.SuppressUpdateNotice, true); window.showInformationMessage(`GitLens v4 adds many new settings and removes a few old ones, so please review your settings to ensure they are configured properly.`);
} }
} }
async function notifyOnNewGitLensVersion(context: ExtensionContext, version: string) {
if (context.globalState.get(SuppressedKeys.UpdateNotice, false)) return;
const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion);
if (previousVersion === undefined) {
await Messages.showWelcomeMessage();
return;
}
const [major, minor] = version.split('.');
const [prevMajor, prevMinor] = previousVersion.split('.');
if (major === prevMajor && minor === prevMinor) return;
await Messages.showUpdateMessage(version);
}
async function notifyOnUnsupportedGitVersion(context: ExtensionContext, version: string) { async function notifyOnUnsupportedGitVersion(context: ExtensionContext, version: string) {
if (context.globalState.get(WorkspaceState.SuppressGitVersionWarning, false)) return; if (GitService.validateGitVersion(2, 2)) return;
// If git is less than v2.2.0 // If git is less than v2.2.0
if (!GitService.validateGitVersion(2, 2)) { await Messages.showUnsupportedGitVersionErrorMessage(version);
const result = await window.showErrorMessage(`GitLens requires a newer version of Git (>= 2.2.0) than is currently installed (${version}). Please install a more recent version of Git.`, `Don't Show Again`);
if (result === `Don't Show Again`) {
context.globalState.update(WorkspaceState.SuppressGitVersionWarning, true);
}
}
} }

View File

@@ -0,0 +1,160 @@
'use strict';
import { Strings } from '../../system';
import { GitCommit } from '../models/commit';
import { GitDiffLine } 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.interpolate(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}\` &nbsp; __${commit.author}__, ${moment(commit.date).fromNow()} &nbsp; _(${moment(commit.date).format(dateFormat)})_${message}`;
}
static toHoverDiff(commit: GitCommit, previous: GitDiffLine | undefined, current: GitDiffLine | undefined): string | undefined {
if (previous === undefined && current === undefined) return undefined;
const codeDiff = this._getCodeDiff(previous, current);
return commit.isUncommitted
? `\`Changes\` &nbsp; \u2014 &nbsp; _uncommitted_\n${codeDiff}`
: `\`Changes\` &nbsp; \u2014 &nbsp; \`${commit.previousShortSha}\` \u2194 \`${commit.shortSha}\`\n${codeDiff}`;
}
private static _getCodeDiff(previous: GitDiffLine | undefined, current: GitDiffLine | undefined): string {
return `\`\`\`
- ${previous === undefined ? '' : previous.line.trim()}
+ ${current === undefined ? '' : current.line.trim()}
\`\`\``;
}
}

View File

@@ -62,8 +62,8 @@ export class GitContextTracker extends Disposable {
} }
private _onBlameFailed(key: string) { private _onBlameFailed(key: string) {
const fileName = this._editor && this._editor.document && this._editor.document.fileName; if (this._editor === undefined || this._editor.document === undefined || this._editor.document.uri === undefined) return;
if (!fileName || key !== this.git.getCacheEntryKey(fileName)) return; if (key !== this.git.getCacheEntryKey(this._editor.document.uri)) return;
this._updateBlameability(false); this._updateBlameability(false);
} }
@@ -75,6 +75,7 @@ export class GitContextTracker extends Disposable {
// this._unsubscribeToDocumentChanges(); // this._unsubscribeToDocumentChanges();
// this.updateBlameability(false); // this.updateBlameability(false);
// TODO: Rework this once https://github.com/Microsoft/vscode/issues/27231 is released in v1.13
// We have to defer because isDirty is not reliable inside this event // We have to defer because isDirty is not reliable inside this event
setTimeout(() => this._updateBlameability(!e.document.isDirty), 1); setTimeout(() => this._updateBlameability(!e.document.isDirty), 1);
} }
@@ -113,13 +114,15 @@ export class GitContextTracker extends Disposable {
private async _updateContextHasRemotes(uri: GitUri | undefined) { private async _updateContextHasRemotes(uri: GitUri | undefined) {
try { try {
let repoPath = this.git.repoPath;
if (uri !== undefined && this.git.isTrackable(uri)) {
repoPath = uri.repoPath || this.git.repoPath;
}
let hasRemotes = false; let hasRemotes = false;
if (uri && this.git.isTrackable(uri)) { if (repoPath) {
const repoPath = uri.repoPath || this.git.repoPath; const remotes = await this.git.getRemotes(repoPath);
if (repoPath) { hasRemotes = remotes.length !== 0;
const remotes = await this.git.getRemotes(repoPath);
hasRemotes = remotes.length !== 0;
}
} }
setCommandContext(CommandContext.HasRemotes, hasRemotes); setCommandContext(CommandContext.HasRemotes, hasRemotes);

View File

@@ -83,15 +83,15 @@ export class GitUri extends Uri {
if (!git.isTrackable(uri)) return new GitUri(uri, git.repoPath); if (!git.isTrackable(uri)) return new GitUri(uri, git.repoPath);
const gitUri = git.getGitUriForFile(uri.fsPath);
if (gitUri) return gitUri;
// If this is a git uri, assume it is showing the most recent commit // If this is a git uri, assume it is showing the most recent commit
if (uri.scheme === DocumentSchemes.Git && uri.query === '~') { if (uri.scheme === DocumentSchemes.Git) {
const commit = await git.getLogCommit(undefined, uri.fsPath); const commit = await git.getLogCommit(undefined, uri.fsPath);
if (commit) return new GitUri(uri, commit); if (commit !== undefined) return new GitUri(uri, commit);
} }
const gitUri = git.getGitUriForFile(uri);
if (gitUri) return gitUri;
return new GitUri(uri, (await git.getRepoPathFromFile(uri.fsPath)) || git.repoPath); return new GitUri(uri, (await git.getRepoPathFromFile(uri.fsPath)) || git.repoPath);
} }

View File

@@ -1,25 +1,26 @@
'use strict'; 'use strict';
import { GitCommit, IGitAuthor, IGitCommitLine } from './commit'; import { GitAuthor, GitCommitLine } from './commit';
import { GitBlameCommit } from './blameCommit';
export interface IGitBlame { export interface GitBlame {
repoPath: string; repoPath: string;
authors: Map<string, IGitAuthor>; authors: Map<string, GitAuthor>;
commits: Map<string, GitCommit>; commits: Map<string, GitBlameCommit>;
lines: IGitCommitLine[]; lines: GitCommitLine[];
} }
export interface IGitBlameLine { export interface GitBlameLine {
author: IGitAuthor; author: GitAuthor;
commit: GitCommit; commit: GitBlameCommit;
line: IGitCommitLine; line: GitCommitLine;
} }
export interface IGitBlameLines extends IGitBlame { export interface GitBlameLines extends GitBlame {
allLines: IGitCommitLine[]; allLines: GitCommitLine[];
} }
export interface IGitBlameCommitLines { export interface GitBlameCommitLines {
author: IGitAuthor; author: GitAuthor;
commit: GitCommit; commit: GitBlameCommit;
lines: IGitCommitLine[]; lines: GitCommitLine[];
} }

View File

@@ -0,0 +1,20 @@
'use strict';
import { GitCommit, GitCommitLine } from './commit';
export class GitBlameCommit extends GitCommit {
constructor(
repoPath: string,
sha: string,
fileName: string,
author: string,
date: Date,
message: string,
public lines: GitCommitLine[],
originalFileName?: string,
previousSha?: string,
previousFileName?: string
) {
super('blame', repoPath, sha, fileName, author, date, message, originalFileName, previousSha, previousFileName);
}
}

View File

@@ -3,30 +3,12 @@ import { Uri } from 'vscode';
import { Git } from '../git'; import { Git } from '../git';
import * as path from 'path'; import * as path from 'path';
export interface IGitAuthor { export interface GitAuthor {
name: string; name: string;
lineCount: number; lineCount: number;
} }
export interface IGitCommit { export interface GitCommitLine {
type: GitCommitType;
repoPath: string;
sha: string;
fileName: string;
author?: string;
date: Date;
message: string;
lines: IGitCommitLine[];
originalFileName?: string;
previousSha?: string;
previousFileName?: string;
readonly isUncommitted: boolean;
previousUri: Uri;
uri: Uri;
}
export interface IGitCommitLine {
sha: string; sha: string;
previousSha?: string; previousSha?: string;
line: number; line: number;
@@ -36,10 +18,10 @@ export interface IGitCommitLine {
export type GitCommitType = 'blame' | 'branch' | 'file' | 'stash'; export type GitCommitType = 'blame' | 'branch' | 'file' | 'stash';
export class GitCommit implements IGitCommit { export class GitCommit {
type: GitCommitType; type: GitCommitType;
lines: IGitCommitLine[]; // lines: GitCommitLine[];
originalFileName?: string; originalFileName?: string;
previousSha?: string; previousSha?: string;
previousFileName?: string; previousFileName?: string;
@@ -54,7 +36,7 @@ export class GitCommit implements IGitCommit {
public author: string, public author: string,
public date: Date, public date: Date,
public message: string, public message: string,
lines?: IGitCommitLine[], // lines?: GitCommitLine[],
originalFileName?: string, originalFileName?: string,
previousSha?: string, previousSha?: string,
previousFileName?: string previousFileName?: string
@@ -62,7 +44,7 @@ export class GitCommit implements IGitCommit {
this.type = type; this.type = type;
this.fileName = this.fileName && this.fileName.replace(/, ?$/, ''); this.fileName = this.fileName && this.fileName.replace(/, ?$/, '');
this.lines = lines || []; // this.lines = lines || [];
this.originalFileName = originalFileName; this.originalFileName = originalFileName;
this.previousSha = previousSha; this.previousSha = previousSha;
this.previousFileName = previousFileName; this.previousFileName = previousFileName;

View File

@@ -1,18 +1,45 @@
'use strict'; 'use strict';
import { GitDiffParser } from '../parsers/diffParser';
export interface IGitDiffChunk { export interface GitDiffLine {
chunk?: string; line: string;
state: 'added' | 'removed' | 'unchanged';
original: (string | undefined)[];
originalStart: number;
originalEnd: number;
changes: (string | undefined)[];
changesStart: number;
changesEnd: number;
} }
export interface IGitDiff { export class GitDiffChunk {
private _chunk: string | undefined;
private _current: (GitDiffLine | undefined)[] | undefined;
private _previous: (GitDiffLine | undefined)[] | undefined;
constructor(chunk: string, public currentPosition: { start: number, end: number }, public previousPosition: { start: number, end: number }) {
this._chunk = chunk;
}
get current(): (GitDiffLine | undefined)[] {
if (this._chunk !== undefined) {
this.parseChunk();
}
return this._current!;
}
get previous(): (GitDiffLine | undefined)[] {
if (this._chunk !== undefined) {
this.parseChunk();
}
return this._previous!;
}
private parseChunk() {
[this._current, this._previous] = GitDiffParser.parseChunk(this._chunk!);
this._chunk = undefined;
}
}
export interface GitDiff {
chunks: GitDiffChunk[];
diff?: string; diff?: string;
chunks: IGitDiffChunk[];
} }

View File

@@ -1,11 +1,11 @@
'use strict'; 'use strict';
import { Range } from 'vscode'; import { Range } from 'vscode';
import { IGitAuthor } from './commit'; import { GitAuthor } from './commit';
import { GitLogCommit } from './logCommit'; import { GitLogCommit } from './logCommit';
export interface IGitLog { export interface GitLog {
repoPath: string; repoPath: string;
authors: Map<string, IGitAuthor>; authors: Map<string, GitAuthor>;
commits: Map<string, GitLogCommit>; commits: Map<string, GitLogCommit>;
sha: string | undefined; sha: string | undefined;

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import { GitCommit, GitCommitType, IGitCommitLine } from './commit'; import { GitCommit, GitCommitType } from './commit';
import { GitStatusFileStatus, IGitStatusFile } from './status'; import { GitStatusFileStatus, IGitStatusFile } from './status';
import * as path from 'path'; import * as path from 'path';
@@ -23,12 +23,11 @@ export class GitLogCommit extends GitCommit {
message: string, message: string,
status?: GitStatusFileStatus, status?: GitStatusFileStatus,
fileStatuses?: IGitStatusFile[], fileStatuses?: IGitStatusFile[],
lines?: IGitCommitLine[],
originalFileName?: string, originalFileName?: string,
previousSha?: string, previousSha?: string,
previousFileName?: string previousFileName?: string
) { ) {
super(type, repoPath, sha, fileName, author, date, message, lines, originalFileName, previousSha, previousFileName); super(type, repoPath, sha, fileName, author, date, message, originalFileName, previousSha, previousFileName);
this.fileNames = this.fileName; this.fileNames = this.fileName;

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
export * from './blame'; export * from './blame';
export * from './blameCommit';
export * from './branch'; export * from './branch';
export * from './commit'; export * from './commit';
export * from './diff'; export * from './diff';

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { GitStashCommit } from './stashCommit'; import { GitStashCommit } from './stashCommit';
export interface IGitStash { export interface GitStash {
repoPath: string; repoPath: string;
commits: Map<string, GitStashCommit>; commits: Map<string, GitStashCommit>;
} }

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
import { IGitCommitLine } from './commit';
import { GitLogCommit } from './logCommit'; import { GitLogCommit } from './logCommit';
import { GitStatusFileStatus, IGitStatusFile } from './status'; import { GitStatusFileStatus, IGitStatusFile } from './status';
@@ -14,12 +13,11 @@ export class GitStashCommit extends GitLogCommit {
message: string, message: string,
status?: GitStatusFileStatus, status?: GitStatusFileStatus,
fileStatuses?: IGitStatusFile[], fileStatuses?: IGitStatusFile[],
lines?: IGitCommitLine[],
originalFileName?: string, originalFileName?: string,
previousSha?: string, previousSha?: string,
previousFileName?: string previousFileName?: string
) { ) {
super('stash', repoPath, sha, fileName, 'You', date, message, status, fileStatuses, lines, originalFileName, previousSha, previousFileName); super('stash', repoPath, sha, fileName, 'You', date, message, status, fileStatuses, originalFileName, previousSha, previousFileName);
} }
get shortSha() { get shortSha() {

View File

@@ -2,7 +2,7 @@
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import * as path from 'path'; import * as path from 'path';
export interface IGitStatus { export interface GitStatus {
branch: string; branch: string;
repoPath: string; repoPath: string;

View File

@@ -1,9 +1,10 @@
'use strict'; 'use strict';
import { Git, GitCommit, IGitAuthor, IGitBlame, IGitCommitLine } from './../git'; import { Strings } from '../../system';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitCommitLine } from './../git';
import * as moment from 'moment'; import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
interface IBlameEntry { interface BlameEntry {
sha: string; sha: string;
line: number; line: number;
@@ -11,15 +12,9 @@ interface IBlameEntry {
lineCount: number; lineCount: number;
author: string; author: string;
// authorEmail?: string;
authorDate?: string; authorDate?: string;
authorTimeZone?: string; authorTimeZone?: string;
// committer?: string;
// committerEmail?: string;
// committerDate?: string;
// committerTimeZone?: string;
previousSha?: string; previousSha?: string;
previousFileName?: string; previousFileName?: string;
@@ -30,21 +25,26 @@ interface IBlameEntry {
export class GitBlameParser { export class GitBlameParser {
private static _parseEntries(data: string): IBlameEntry[] | undefined { static parse(data: string, repoPath: string | undefined, fileName: string): GitBlame | undefined {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n'); const authors: Map<string, GitAuthor> = new Map();
if (!lines.length) return undefined; const commits: Map<string, GitBlameCommit> = new Map();
const lines: GitCommitLine[] = [];
const entries: IBlameEntry[] = []; let relativeFileName = repoPath && fileName;
let entry: IBlameEntry | undefined = undefined; let entry: BlameEntry | undefined = undefined;
let position = -1; let line: string;
while (++position < lines.length) { let lineParts: string[];
const lineParts = lines[position].split(' ');
if (lineParts.length < 2) { let i = -1;
continue; let first = true;
}
for (line of Strings.lines(data)) {
i++;
lineParts = line.split(' ');
if (lineParts.length < 2) continue;
if (entry === undefined) { if (entry === undefined) {
entry = { entry = {
@@ -52,7 +52,7 @@ export class GitBlameParser {
originalLine: parseInt(lineParts[1], 10) - 1, originalLine: parseInt(lineParts[1], 10) - 1,
line: parseInt(lineParts[2], 10) - 1, line: parseInt(lineParts[2], 10) - 1,
lineCount: parseInt(lineParts[3], 10) lineCount: parseInt(lineParts[3], 10)
} as IBlameEntry; } as BlameEntry;
continue; continue;
} }
@@ -60,14 +60,10 @@ export class GitBlameParser {
switch (lineParts[0]) { switch (lineParts[0]) {
case 'author': case 'author':
entry.author = Git.isUncommitted(entry.sha) entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted' ? 'You'
: lineParts.slice(1).join(' ').trim(); : lineParts.slice(1).join(' ').trim();
break; break;
// case 'author-mail':
// entry.authorEmail = lineParts[1].trim();
// break;
case 'author-time': case 'author-time':
entry.authorDate = lineParts[1]; entry.authorDate = lineParts[1];
break; break;
@@ -76,22 +72,6 @@ export class GitBlameParser {
entry.authorTimeZone = lineParts[1]; entry.authorTimeZone = lineParts[1];
break; break;
// case 'committer':
// entry.committer = lineParts.slice(1).join(' ').trim();
// break;
// case 'committer-mail':
// entry.committerEmail = lineParts[1].trim();
// break;
// case 'committer-time':
// entry.committerDate = lineParts[1];
// break;
// case 'committer-tz':
// entry.committerTimeZone = lineParts[1];
// break;
case 'summary': case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim(); entry.summary = lineParts.slice(1).join(' ').trim();
break; break;
@@ -104,7 +84,15 @@ export class GitBlameParser {
case 'filename': case 'filename':
entry.fileName = lineParts.slice(1).join(' '); entry.fileName = lineParts.slice(1).join(' ');
entries.push(entry); if (first && repoPath === undefined) {
// Try to get the repoPath from the most recent commit
repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
relativeFileName = Git.normalizePath(path.relative(repoPath, fileName));
}
first = false;
GitBlameParser._parseEntry(entry, repoPath, relativeFileName, commits, authors, lines);
entry = undefined; entry = undefined;
break; break;
@@ -113,71 +101,6 @@ export class GitBlameParser {
} }
} }
return entries;
}
static parse(data: string, repoPath: string | undefined, fileName: string): IGitBlame | undefined {
const entries = this._parseEntries(data);
if (!entries) return undefined;
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, GitCommit> = new Map();
const lines: IGitCommitLine[] = [];
let relativeFileName = repoPath && fileName;
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
if (i === 0 && repoPath === undefined) {
// Try to get the repoPath from the most recent commit
repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
relativeFileName = Git.normalizePath(path.relative(repoPath, fileName));
}
let commit = commits.get(entry.sha);
if (commit === undefined) {
if (entry.author !== undefined) {
let author = authors.get(entry.author);
if (author === undefined) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
}
commit = new GitCommit('blame', repoPath!, entry.sha, relativeFileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!);
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
if (entry.previousSha) {
commit.previousSha = entry.previousSha;
commit.previousFileName = entry.previousFileName;
}
commits.set(entry.sha, commit);
}
for (let j = 0, len = entry.lineCount; j < len; j++) {
const line: IGitCommitLine = {
sha: entry.sha,
line: entry.line + j,
originalLine: entry.originalLine + j
};
if (commit.previousSha) {
line.previousSha = commit.previousSha;
}
commit.lines.push(line);
lines[line.line] = line;
}
}
commits.forEach(c => { commits.forEach(c => {
if (c.author === undefined) return; if (c.author === undefined) return;
@@ -187,23 +110,57 @@ export class GitBlameParser {
author.lineCount += c.lines.length; author.lineCount += c.lines.length;
}); });
const sortedAuthors: Map<string, IGitAuthor> = new Map(); const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount));
// const values =
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
// const sortedCommits: Map<string, IGitCommit> = new Map();
// Array.from(commits.values())
// .sort((a, b) => b.date.getTime() - a.date.getTime())
// .forEach(c => sortedCommits.set(c.sha, c));
return { return {
repoPath: repoPath, repoPath: repoPath,
authors: sortedAuthors, authors: sortedAuthors,
// commits: sortedCommits,
commits: commits, commits: commits,
lines: lines lines: lines
} as IGitBlame; } as GitBlame;
}
private static _parseEntry(entry: BlameEntry, repoPath: string | undefined, fileName: string | undefined, commits: Map<string, GitBlameCommit>, authors: Map<string, GitAuthor>, lines: GitCommitLine[]) {
let commit = commits.get(entry.sha);
if (commit === undefined) {
if (entry.author !== undefined) {
let author = authors.get(entry.author);
if (author === undefined) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
}
commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!, []);
if (fileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
if (entry.previousSha) {
commit.previousSha = entry.previousSha;
commit.previousFileName = entry.previousFileName;
}
commits.set(entry.sha, commit);
}
for (let i = 0, len = entry.lineCount; i < len; i++) {
const line: GitCommitLine = {
sha: entry.sha,
line: entry.line + i,
originalLine: entry.originalLine + i
};
if (commit.previousSha) {
line.previousSha = commit.previousSha;
}
commit.lines.push(line);
lines[line.line] = line;
}
} }
} }

View File

@@ -1,37 +1,33 @@
'use strict'; 'use strict';
import { IGitDiff, IGitDiffChunk } from './../git'; import { Iterables, Strings } from '../../system';
import { GitDiff, GitDiffChunk, GitDiffLine } from './../git';
const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm;
export class GitDiffParser { export class GitDiffParser {
static parse(data: string, debug: boolean = false): IGitDiff | undefined { static parse(data: string, debug: boolean = false): GitDiff | undefined {
if (!data) return undefined; if (!data) return undefined;
const chunks: IGitDiffChunk[] = []; const chunks: GitDiffChunk[] = [];
let match: RegExpExecArray | null = null; let match: RegExpExecArray | null = null;
let chunk: string;
let currentStart: number;
let previousStart: number;
do { do {
match = unifiedDiffRegex.exec(`${data}\n@@`); match = unifiedDiffRegex.exec(`${data}\n@@`);
if (match == null) break; if (match == null) break;
const originalStart = +match[1]; // Stops excessive memory usage
const changedStart = +match[3]; // https://bugs.chromium.org/p/v8/issues/detail?id=2869
chunk = (' ' + match[5]).substr(1);
currentStart = parseInt(match[3], 10);
previousStart = parseInt(match[1], 10);
const chunk = match[5]; chunks.push(new GitDiffChunk(chunk, { start: currentStart, end: currentStart + parseInt(match[4], 10) }, { start: previousStart, end: previousStart + parseInt(match[2], 10) }));
const lines = chunk.split('\n').slice(1);
const original = lines.filter(l => l[0] !== '+').map(l => (l[0] === '-') ? l.substring(1) : undefined);
const changed = lines.filter(l => l[0] !== '-').map(l => (l[0] === '+') ? l.substring(1) : undefined);
chunks.push({
chunk: debug ? chunk : undefined,
original: original,
originalStart: originalStart,
originalEnd: originalStart + +match[2],
changes: changed,
changesStart: changedStart,
changesEnd: changedStart + +match[4]
});
} while (match != null); } while (match != null);
if (!chunks.length) return undefined; if (!chunks.length) return undefined;
@@ -39,7 +35,40 @@ export class GitDiffParser {
const diff = { const diff = {
diff: debug ? data : undefined, diff: debug ? data : undefined,
chunks: chunks chunks: chunks
} as IGitDiff; } as GitDiff;
return diff; return diff;
} }
static parseChunk(chunk: string): [(GitDiffLine | undefined)[], (GitDiffLine | undefined)[]] {
const lines = Iterables.skip(Strings.lines(chunk), 1);
const current: (GitDiffLine | undefined)[] = [];
const previous: (GitDiffLine | undefined)[] = [];
for (const l of lines) {
switch (l[0]) {
case '+':
current.push({
line: ` ${l.substring(1)}`,
state: 'added'
});
previous.push(undefined);
break;
case '-':
current.push(undefined);
previous.push({
line: ` ${l.substring(1)}`,
state: 'removed'
});
break;
default:
current.push({ line: l, state: 'unchanged' });
previous.push({ line: l, state: 'unchanged' });
break;
}
}
return [current, previous];
}
} }

View File

@@ -1,19 +1,17 @@
'use strict'; 'use strict';
import { Strings } from '../../system';
import { Range } from 'vscode'; import { Range } from 'vscode';
import { Git, GitCommitType, GitLogCommit, GitStatusFileStatus, IGitAuthor, IGitLog, IGitStatusFile } from './../git'; import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger'; // import { Logger } from '../../logger';
import * as moment from 'moment'; import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
interface ILogEntry { interface LogEntry {
sha: string; sha: string;
author: string; author: string;
authorDate?: string; authorDate?: string;
// committer?: string;
// committerDate?: string;
parentShas?: string[]; parentShas?: string[];
fileName?: string; fileName?: string;
@@ -29,31 +27,54 @@ const diffRegex = /diff --git a\/(.*) b\/(.*)/;
export class GitLogParser { export class GitLogParser {
private static _parseEntries(data: string, type: GitCommitType, maxCount: number | undefined, reverse: boolean): ILogEntry[] | undefined { static parse(data: string, type: GitCommitType, repoPath: string | undefined, fileName: string | undefined, sha: string | undefined, maxCount: number | undefined, reverse: boolean, range: Range | undefined): GitLog | undefined {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n'); const authors: Map<string, GitAuthor> = new Map();
if (!lines.length) return undefined; const commits: Map<string, GitLogCommit> = new Map();
const entries: ILogEntry[] = []; let relativeFileName: string;
let recentCommit: GitLogCommit | undefined = undefined;
let entry: ILogEntry | undefined = undefined; if (repoPath !== undefined) {
let position = -1; repoPath = Git.normalizePath(repoPath);
while (++position < lines.length) { }
// Since log --reverse doesn't properly honor a max count -- enforce it here
if (reverse && maxCount && (entries.length >= maxCount)) break;
let lineParts = lines[position].split(' '); let entry: LogEntry | undefined = undefined;
if (lineParts.length < 2) { let line: string | undefined = undefined;
continue; let lineParts: string[];
let next: IteratorResult<string> | undefined = undefined;
let i = -1;
let first = true;
let skip = false;
const lines = Strings.lines(data);
// for (line of lines) {
while (true) {
if (!skip) {
next = lines.next();
if (next.done) break;
line = next.value;
i++;
} }
else {
skip = false;
}
// Since log --reverse doesn't properly honor a max count -- enforce it here
if (reverse && maxCount && (i >= maxCount)) break;
lineParts = line!.split(' ');
if (lineParts.length < 2) continue;
if (entry === undefined) { if (entry === undefined) {
if (!Git.shaRegex.test(lineParts[0])) continue; if (!Git.shaRegex.test(lineParts[0])) continue;
entry = { entry = {
sha: lineParts[0] sha: lineParts[0]
} as ILogEntry; } as LogEntry;
continue; continue;
} }
@@ -61,7 +82,7 @@ export class GitLogParser {
switch (lineParts[0]) { switch (lineParts[0]) {
case 'author': case 'author':
entry.author = Git.isUncommitted(entry.sha) entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted' ? 'You'
: lineParts.slice(1).join(' ').trim(); : lineParts.slice(1).join(' ').trim();
break; break;
@@ -69,47 +90,54 @@ export class GitLogParser {
entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`; entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`;
break; break;
// case 'committer':
// entry.committer = lineParts.slice(1).join(' ').trim();
// break;
// case 'committer-date':
// entry.committerDate = lineParts.slice(1).join(' ').trim();
// break;
case 'parents': case 'parents':
entry.parentShas = lineParts.slice(1); entry.parentShas = lineParts.slice(1);
break; break;
case 'summary': case 'summary':
entry.summary = lineParts.slice(1).join(' ').trim(); entry.summary = lineParts.slice(1).join(' ').trim();
while (++position < lines.length) { while (true) {
const next = lines[position]; next = lines.next();
if (!next) break; if (next.done) break;
if (next === 'filename ?') {
position--; i++;
line = next.value;
if (!line) break;
if (line === 'filename ?') {
skip = true;
break; break;
} }
entry.summary += `\n${lines[position]}`; entry.summary += `\n${line}`;
} }
break; break;
case 'filename': case 'filename':
if (type === 'branch') { if (type === 'branch') {
const nextLine = lines[position + 1]; next = lines.next();
// If the next line isn't blank, make sure it isn't starting a new commit if (next.done) break;
if (nextLine && Git.shaRegex.test(nextLine)) continue;
position++; i++;
line = next.value;
// If the next line isn't blank, make sure it isn't starting a new commit
if (line && Git.shaRegex.test(line)) {
skip = true;
continue;
}
let diff = false; let diff = false;
while (++position < lines.length) { while (true) {
const line = lines[position]; next = lines.next();
if (next.done) break;
i++;
line = next.value;
lineParts = line.split(' '); lineParts = line.split(' ');
if (Git.shaRegex.test(lineParts[0])) { if (Git.shaRegex.test(lineParts[0])) {
position--; skip = true;
break; break;
} }
@@ -147,125 +175,88 @@ export class GitLogParser {
} }
} }
else { else {
position += 2; next = lines.next();
const line = lines[position]; next = lines.next();
i += 2;
line = next.value;
entry.status = line[0] as GitStatusFileStatus; entry.status = line[0] as GitStatusFileStatus;
entry.fileName = line.substring(1); entry.fileName = line.substring(1);
this._parseFileName(entry); this._parseFileName(entry);
} }
entries.push(entry); if (first && repoPath === undefined && type === 'file' && fileName !== undefined) {
// Try to get the repoPath from the most recent commit
repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
relativeFileName = Git.normalizePath(path.relative(repoPath, fileName));
}
else {
relativeFileName = entry.fileName!;
}
first = false;
recentCommit = GitLogParser._parseEntry(entry, type, repoPath, relativeFileName, commits, authors, recentCommit);
entry = undefined; entry = undefined;
break; break;
default:
break;
} }
if (next!.done) break;
} }
return entries;
}
static parse(data: string, type: GitCommitType, repoPath: string | undefined, fileName: string | undefined, sha: string | undefined, maxCount: number | undefined, reverse: boolean, range: Range | undefined): IGitLog | undefined {
const entries = this._parseEntries(data, type, maxCount, reverse);
if (!entries) return undefined;
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, GitLogCommit> = new Map();
let relativeFileName: string;
let recentCommit: GitLogCommit | undefined = undefined;
if (repoPath !== undefined) {
repoPath = Git.normalizePath(repoPath);
}
for (let i = 0, len = entries.length; i < len; i++) {
// Since log --reverse doesn't properly honor a max count -- enforce it here
if (reverse && maxCount && (i >= maxCount)) break;
const entry = entries[i];
if (i === 0 && repoPath === undefined && type === 'file' && fileName !== undefined) {
// Try to get the repoPath from the most recent commit
repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, ''));
relativeFileName = Git.normalizePath(path.relative(repoPath, fileName));
}
else {
relativeFileName = entry.fileName!;
}
let commit = commits.get(entry.sha);
if (commit === undefined) {
if (entry.author !== undefined) {
let author = authors.get(entry.author);
if (author === undefined) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
}
commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName);
commit.parentShas = entry.parentShas!;
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
commits.set(entry.sha, commit);
}
// else {
// Logger.log(`merge commit? ${entry.sha}`);
// }
if (recentCommit !== undefined) {
recentCommit.previousSha = commit.sha;
// If the commit sha's match (merge commit), just forward it along
commit.nextSha = commit.sha !== recentCommit.sha ? recentCommit.sha : recentCommit.nextSha;
// Only add a filename if this is a file log
if (type === 'file') {
recentCommit.previousFileName = commit.originalFileName || commit.fileName;
commit.nextFileName = recentCommit.originalFileName || recentCommit.fileName;
}
}
recentCommit = commit;
}
commits.forEach(c => {
if (c.author === undefined) return;
const author = authors.get(c.author);
if (author === undefined) return;
author.lineCount += c.lines.length;
});
const sortedAuthors: Map<string, IGitAuthor> = new Map();
// const values =
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
// const sortedCommits: Map<string, IGitCommit> = new Map();
// Array.from(commits.values())
// .sort((a, b) => b.date.getTime() - a.date.getTime())
// .forEach(c => sortedCommits.set(c.sha, c));
return { return {
repoPath: repoPath, repoPath: repoPath,
authors: sortedAuthors, authors: authors,
// commits: sortedCommits,
commits: commits, commits: commits,
sha: sha, sha: sha,
maxCount: maxCount, maxCount: maxCount,
range: range, range: range,
truncated: !!(maxCount && entries.length >= maxCount) truncated: !!(maxCount && i >= maxCount)
} as IGitLog; } as GitLog;
}
private static _parseEntry(entry: LogEntry, type: GitCommitType, repoPath: string | undefined, relativeFileName: string, commits: Map<string, GitLogCommit>, authors: Map<string, GitAuthor>, recentCommit: GitLogCommit | undefined): GitLogCommit | undefined {
let commit = commits.get(entry.sha);
if (commit === undefined) {
if (entry.author !== undefined) {
let author = authors.get(entry.author);
if (author === undefined) {
author = {
name: entry.author,
lineCount: 0
};
authors.set(entry.author, author);
}
}
commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName);
commit.parentShas = entry.parentShas!;
if (relativeFileName !== entry.fileName) {
commit.originalFileName = entry.fileName;
}
commits.set(entry.sha, commit);
}
// else {
// Logger.log(`merge commit? ${entry.sha}`);
// }
if (recentCommit !== undefined) {
recentCommit.previousSha = commit.sha;
// If the commit sha's match (merge commit), just forward it along
commit.nextSha = commit.sha !== recentCommit.sha ? recentCommit.sha : recentCommit.nextSha;
// Only add a filename if this is a file log
if (type === 'file') {
recentCommit.previousFileName = commit.originalFileName || commit.fileName;
commit.nextFileName = recentCommit.originalFileName || recentCommit.fileName;
}
}
return commit;
} }
private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) { private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) {

View File

@@ -1,9 +1,9 @@
'use strict'; 'use strict';
import { Git, GitStashCommit, GitStatusFileStatus, IGitStash, IGitStatusFile } from './../git'; import { Git, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger'; // import { Logger } from '../../logger';
import * as moment from 'moment'; import * as moment from 'moment';
interface IStashEntry { interface StashEntry {
sha: string; sha: string;
date?: string; date?: string;
fileNames: string; fileNames: string;
@@ -14,15 +14,15 @@ interface IStashEntry {
export class GitStashParser { export class GitStashParser {
private static _parseEntries(data: string): IStashEntry[] | undefined { private static _parseEntries(data: string): StashEntry[] | undefined {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n'); const lines = data.split('\n');
if (!lines.length) return undefined; if (!lines.length) return undefined;
const entries: IStashEntry[] = []; const entries: StashEntry[] = [];
let entry: IStashEntry | undefined = undefined; let entry: StashEntry | undefined = undefined;
let position = -1; let position = -1;
while (++position < lines.length) { while (++position < lines.length) {
let lineParts = lines[position].split(' '); let lineParts = lines[position].split(' ');
@@ -35,7 +35,7 @@ export class GitStashParser {
entry = { entry = {
sha: lineParts[0] sha: lineParts[0]
} as IStashEntry; } as StashEntry;
continue; continue;
} }
@@ -109,7 +109,7 @@ export class GitStashParser {
return entries; return entries;
} }
static parse(data: string, repoPath: string): IGitStash | undefined { static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this._parseEntries(data); const entries = this._parseEntries(data);
if (entries === undefined) return undefined; if (entries === undefined) return undefined;
@@ -128,7 +128,7 @@ export class GitStashParser {
return { return {
repoPath: repoPath, repoPath: repoPath,
commits: commits commits: commits
} as IGitStash; } as GitStash;
} }
private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) { private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) {

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { Git, GitStatusFile, GitStatusFileStatus, IGitStatus } from './../git'; import { Git, GitStatus, GitStatusFile, GitStatusFileStatus } from './../git';
interface IFileStatusEntry { interface FileStatusEntry {
staged: boolean; staged: boolean;
status: GitStatusFileStatus; status: GitStatusFileStatus;
fileName: string; fileName: string;
@@ -13,7 +13,7 @@ const behindStatusV1Regex = /(?:behind ([0-9]+))/;
export class GitStatusParser { export class GitStatusParser {
static parse(data: string, repoPath: string, porcelainVersion: number): IGitStatus | undefined { static parse(data: string, repoPath: string, porcelainVersion: number): GitStatus | undefined {
if (!data) return undefined; if (!data) return undefined;
const lines = data.split('\n').filter(_ => !!_); const lines = data.split('\n').filter(_ => !!_);
@@ -40,7 +40,7 @@ export class GitStatusParser {
return status; return status;
} }
private static _parseV1(lines: string[], repoPath: string, status: IGitStatus) { private static _parseV1(lines: string[], repoPath: string, status: GitStatus) {
let position = -1; let position = -1;
while (++position < lines.length) { while (++position < lines.length) {
const line = lines[position]; const line = lines[position];
@@ -59,7 +59,7 @@ export class GitStatusParser {
} }
} }
else { else {
let entry: IFileStatusEntry; let entry: FileStatusEntry;
const rawStatus = line.substring(0, 2); const rawStatus = line.substring(0, 2);
const fileName = line.substring(3); const fileName = line.substring(3);
if (rawStatus[0] === 'R') { if (rawStatus[0] === 'R') {
@@ -74,7 +74,7 @@ export class GitStatusParser {
} }
} }
private static _parseV2(lines: string[], repoPath: string, status: IGitStatus) { private static _parseV2(lines: string[], repoPath: string, status: GitStatus) {
let position = -1; let position = -1;
while (++position < lines.length) { while (++position < lines.length) {
const line = lines[position]; const line = lines[position];
@@ -99,7 +99,7 @@ export class GitStatusParser {
} }
else { else {
const lineParts = line.split(' '); const lineParts = line.split(' ');
let entry: IFileStatusEntry | undefined = undefined; let entry: FileStatusEntry | undefined = undefined;
switch (lineParts[0][0]) { switch (lineParts[0][0]) {
case '1': // normal case '1': // normal
entry = this._parseFileEntry(lineParts[1], lineParts.slice(8).join(' ')); entry = this._parseFileEntry(lineParts[1], lineParts.slice(8).join(' '));
@@ -123,7 +123,7 @@ export class GitStatusParser {
} }
} }
private static _parseFileEntry(rawStatus: string, fileName: string, originalFileName?: string): IFileStatusEntry { private static _parseFileEntry(rawStatus: string, fileName: string, originalFileName?: string): FileStatusEntry {
const indexStatus = rawStatus[0] !== '.' ? rawStatus[0].trim() : undefined; const indexStatus = rawStatus[0] !== '.' ? rawStatus[0].trim() : undefined;
const workTreeStatus = rawStatus[1] !== '.' ? rawStatus[1].trim() : undefined; const workTreeStatus = rawStatus[1] !== '.' ? rawStatus[1].trim() : undefined;
@@ -132,6 +132,6 @@ export class GitStatusParser {
fileName: fileName, fileName: fileName,
originalFileName: originalFileName, originalFileName: originalFileName,
staged: !!indexStatus staged: !!indexStatus
} as IFileStatusEntry; } as FileStatusEntry;
} }
} }

View File

@@ -15,7 +15,7 @@ const providerMap = new Map<string, (domain: string, path: string) => RemoteProv
['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)] ['visualstudio.com', (domain: string, path: string) => new VisualStudioService(domain, path)]
]); ]);
const UrlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):\/\/|ssh:\/\/git@(.*?)\/)(.*)$/; const UrlRegex = /^(?:git:\/\/(.*?)\/|https:\/\/(.*?)\/|http:\/\/(.*?)\/|git@(.*):|ssh:\/\/(?:.*@)?(.*?)(?::.*?)?\/)(.*)$/;
export class RemoteProviderFactory { export class RemoteProviderFactory {
@@ -25,7 +25,7 @@ export class RemoteProviderFactory {
if (match == null) return undefined; if (match == null) return undefined;
const domain = match[1] || match[2] || match[3] || match[4] || match[5]; const domain = match[1] || match[2] || match[3] || match[4] || match[5];
const path = match[6].replace(/\.git/, ''); const path = match[6].replace(/\.git\/?$/, '');
const key = domain.toLowerCase().endsWith('visualstudio.com') const key = domain.toLowerCase().endsWith('visualstudio.com')
? 'visualstudio.com' ? 'visualstudio.com'

View File

@@ -1,31 +1,31 @@
'use strict'; 'use strict';
import { Functions, Iterables, Strings } from './system'; import { Functions, Iterables } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; 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 { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands';
import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants'; 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 { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService';
import { Logger } from './logger'; import { Logger } from './logger';
import * as moment from 'moment'; import * as moment from 'moment';
export class GitRecentChangeCodeLens extends CodeLens { export class GitRecentChangeCodeLens extends CodeLens {
constructor(private blame: () => IGitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { constructor(private blame: () => GitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) {
super(range); super(range);
} }
getBlame(): IGitBlameLines | undefined { getBlame(): GitBlameLines | undefined {
return this.blame(); return this.blame();
} }
} }
export class GitAuthorsCodeLens extends CodeLens { export class GitAuthorsCodeLens extends CodeLens {
constructor(private blame: () => IGitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { constructor(private blame: () => GitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) {
super(range); super(range);
} }
getBlame(): IGitBlameLines | undefined { getBlame(): GitBlameLines | undefined {
return this.blame(); return this.blame();
} }
} }
@@ -56,24 +56,22 @@ export class GitCodeLensProvider implements CodeLensProvider {
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> { async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
this._documentIsDirty = document.isDirty; 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) { if (languageLocations == null) {
languageLocations = { languageLocations = {
language: undefined, language: undefined,
location: this._config.codeLens.location, locations: this._config.codeLens.locations,
customSymbols: this._config.codeLens.locationCustomSymbols customSymbols: this._config.codeLens.customLocationSymbols
} as ICodeLensLanguageLocation; } as ICodeLensLanguageLocation;
} }
const lenses: CodeLens[] = []; const lenses: CodeLens[] = [];
if (languageLocations.location === CodeLensLocation.None) return lenses;
const gitUri = await GitUri.fromUri(document.uri, this.git); const gitUri = await GitUri.fromUri(document.uri, this.git);
const blamePromise = this.git.getBlameForFile(gitUri); const blamePromise = this.git.getBlameForFile(gitUri);
let blame: IGitBlame | undefined; let blame: GitBlame | undefined;
if (languageLocations.location === CodeLensLocation.Document) { if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) {
blame = await blamePromise; blame = await blamePromise;
if (blame === undefined || !blame.lines.length) return lenses; if (blame === undefined || !blame.lines.length) return lenses;
} }
@@ -83,7 +81,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<any> commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<any>
]); ]);
blame = values[0] as IGitBlame; blame = values[0] as GitBlame;
if (blame === undefined || !blame.lines.length) return lenses; if (blame === undefined || !blame.lines.length) return lenses;
const symbols = values[1] as SymbolInformation[]; const symbols = values[1] as SymbolInformation[];
@@ -91,11 +89,12 @@ export class GitCodeLensProvider implements CodeLensProvider {
symbols.forEach(sym => this._provideCodeLens(gitUri, document, sym, languageLocations!, blame!, lenses)); 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 // 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)) { if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
let blameForRangeFn: (() => IGitBlameLines | undefined) | undefined = undefined; let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined;
if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri, blameRange)); blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri, blameRange));
lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character))); lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character)));
@@ -117,61 +116,67 @@ export class GitCodeLensProvider implements CodeLensProvider {
private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined { private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined {
let valid = false; let valid = false;
let range: Range | undefined; let range: Range | undefined;
switch (languageLocation.location) {
case CodeLensLocation.All: switch (symbol.kind) {
case CodeLensLocation.DocumentAndContainers: case SymbolKind.File:
switch (symbol.kind) { if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
case SymbolKind.File: valid = true;
valid = true; }
// Adjust the range to be the whole file else if (languageLocation.locations.includes(CodeLensLocations.Custom)) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
break; }
case SymbolKind.Package:
case SymbolKind.Module: if (valid) {
// Adjust the range to be the whole file // 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));
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;
} }
break; 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) { if (valid) {
switch (symbol.kind) { // Adjust the range to be for the whole file
case SymbolKind.File: if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
// Adjust the range to be the whole file range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
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;
} }
} }
break; 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; return valid ? range || symbol.location.range : undefined;
} }
private _provideCodeLens(gitUri: GitUri, document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, blame: IGitBlame, lenses: CodeLens[]): void { private _provideCodeLens(gitUri: GitUri, document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, blame: GitBlame, lenses: CodeLens[]): void {
const blameRange = this._validateSymbolAndGetBlameRange(document, symbol, languageLocation); const blameRange = this._validateSymbolAndGetBlameRange(document, symbol, languageLocation);
if (!blameRange) return; if (!blameRange) return;
@@ -179,19 +184,10 @@ export class GitCodeLensProvider implements CodeLensProvider {
// Make sure there is only 1 lens per line // Make sure there is only 1 lens per line
if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) return; if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) return;
let startChar = -1; // Anchor the code lens to the end of the line -- so they are somewhat consistenly placed
try { let startChar = line.range.end.character - 1;
startChar = line.text.search(`\\b${Strings.escapeRegExp(symbol.name)}\\b`);
}
catch (ex) { }
if (startChar === -1) {
startChar = line.firstNonWhitespaceCharacterIndex;
}
else {
startChar += Math.floor(symbol.name.length / 2);
}
let blameForRangeFn: (() => IGitBlameLines | undefined) | undefined = undefined; let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined;
if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) {
blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri, blameRange)); blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri, blameRange));
lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, symbol.kind, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)))); lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, symbol.kind, blameRange, false, line.range.with(new Position(line.range.start.line, startChar))));
@@ -241,13 +237,13 @@ export class GitCodeLensProvider implements CodeLensProvider {
let title: string; let title: string;
if (this._documentIsDirty) { if (this._documentIsDirty) {
if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) { if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) {
title = 'Cannot determine recent change or authors (unsaved changes)'; title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors;
} }
else if (this._config.codeLens.recentChange.enabled) { else if (this._config.codeLens.recentChange.enabled) {
title = 'Cannot determine recent change (unsaved changes)'; title = this._config.strings.codeLens.unsavedChanges.recentChangeOnly;
} }
else { else {
title = 'Cannot determine authors (unsaved changes)'; title = this._config.strings.codeLens.unsavedChanges.authorsOnly;
} }
lens.command = { title: title } as Command; lens.command = { title: title } as Command;
@@ -260,7 +256,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
const recentCommit = Iterables.first(blame.commits.values()); const recentCommit = Iterables.first(blame.commits.values());
title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`;
if (this._config.codeLens.debug) { if (this._config.codeLens.debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`; title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`;
} }
switch (this._config.codeLens.recentChange.command) { switch (this._config.codeLens.recentChange.command) {
@@ -283,7 +279,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
const count = blame.authors.size; const count = blame.authors.size;
let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`; let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`;
if (this._config.codeLens.debug) { if (this._config.codeLens.debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(Iterables.map(blame.authors.values(), _ => _.name), ', ')})]`; title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join(Iterables.map(blame.authors.values(), _ => _.name), ', ')})]`;
} }
switch (this._config.codeLens.authors.command) { switch (this._config.codeLens.authors.command) {
@@ -299,16 +295,16 @@ export class GitCodeLensProvider implements CodeLensProvider {
} }
} }
_applyBlameAnnotateCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines): T { _applyBlameAnnotateCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines): T {
lens.command = { lens.command = {
title: title, title: title,
command: Commands.ToggleBlame, command: Commands.ToggleFileBlame,
arguments: [Uri.file(lens.uri.fsPath)] arguments: [Uri.file(lens.uri.fsPath)]
}; };
return lens; return lens;
} }
_applyShowBlameHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowBlameHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
let line = lens.range.start.line; let line = lens.range.start.line;
if (commit) { if (commit) {
const blameLine = commit.lines.find(_ => _.line === line); const blameLine = commit.lines.find(_ => _.line === line);
@@ -334,7 +330,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
let line = lens.range.start.line; let line = lens.range.start.line;
if (commit) { if (commit) {
const blameLine = commit.lines.find(_ => _.line === line); const blameLine = commit.lines.find(_ => _.line === line);
@@ -359,8 +355,8 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
if (!commit) { if (commit === undefined) {
const blameLine = blame.allLines[lens.range.start.line]; const blameLine = blame.allLines[lens.range.start.line];
commit = blame.commits.get(blameLine.sha); commit = blame.commits.get(blameLine.sha);
} }
@@ -379,10 +375,10 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickCommitDetails, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath), Uri.file(lens.uri.fsPath),
{ {
@@ -393,10 +389,10 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickCommitFileDetails, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath), Uri.file(lens.uri.fsPath),
{ {
@@ -407,7 +403,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowQuickFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickFileHistory, command: CodeLensCommand.ShowQuickFileHistory,
@@ -421,7 +417,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickBranchHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T { _applyShowQuickBranchHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickCurrentBranchHistory, command: CodeLensCommand.ShowQuickCurrentBranchHistory,

View File

@@ -1,10 +1,10 @@
'use strict'; 'use strict';
import { Iterables, Objects } from './system'; import { Iterables, Objects } from './system';
import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextEditor, Uri, workspace } from 'vscode'; import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode';
import { CommandContext, setCommandContext } from './commands'; import { CommandContext, setCommandContext } from './commands';
import { CodeLensVisibility, IConfig } from './configuration'; import { IConfig } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants'; 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, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitCommit, GitDiff, GitDiffLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git';
import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { GitCodeLensProvider } from './gitCodeLensProvider'; import { GitCodeLensProvider } from './gitCodeLensProvider';
import { Logger } from './logger'; import { Logger } from './logger';
@@ -15,6 +15,7 @@ import * as path from 'path';
export { GitUri, IGitCommitInfo }; export { GitUri, IGitCommitInfo };
export * from './git/models/models'; export * from './git/models/models';
export * from './git/formatters/commit';
export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider'; export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider';
export * from './git/gitContextTracker'; export * from './git/gitContextTracker';
@@ -25,7 +26,7 @@ class UriCacheEntry {
class GitCacheEntry { class GitCacheEntry {
private cache: Map<string, ICachedBlame | ICachedDiff | ICachedLog> = new Map(); private cache: Map<string, CachedBlame | CachedDiff | CachedLog> = new Map();
constructor(public key: string) { } constructor(public key: string) { }
@@ -33,23 +34,23 @@ class GitCacheEntry {
return Iterables.every(this.cache.values(), _ => _.errorMessage !== undefined); return Iterables.every(this.cache.values(), _ => _.errorMessage !== undefined);
} }
get<T extends ICachedBlame | ICachedDiff | ICachedLog>(key: string): T | undefined { get<T extends CachedBlame | CachedDiff | CachedLog>(key: string): T | undefined {
return this.cache.get(key) as T; return this.cache.get(key) as T;
} }
set<T extends ICachedBlame | ICachedDiff | ICachedLog>(key: string, value: T) { set<T extends CachedBlame | CachedDiff | CachedLog>(key: string, value: T) {
this.cache.set(key, value); this.cache.set(key, value);
} }
} }
interface ICachedItem<T> { interface CachedItem<T> {
item: Promise<T>; item: Promise<T>;
errorMessage?: string; errorMessage?: string;
} }
interface ICachedBlame extends ICachedItem<IGitBlame> { } interface CachedBlame extends CachedItem<GitBlame> { }
interface ICachedDiff extends ICachedItem<IGitDiff> { } interface CachedDiff extends CachedItem<GitDiff> { }
interface ICachedLog extends ICachedItem<IGitLog> { } interface CachedLog extends CachedItem<GitLog> { }
enum RemoveCacheReason { enum RemoveCacheReason {
DocumentClosed, DocumentClosed,
@@ -88,7 +89,7 @@ export class GitService extends Disposable {
private _fsWatcher: FileSystemWatcher | undefined; private _fsWatcher: FileSystemWatcher | undefined;
private _gitignore: Promise<ignore.Ignore>; private _gitignore: Promise<ignore.Ignore>;
static EmptyPromise: Promise<IGitBlame | IGitDiff | IGitLog | undefined> = Promise.resolve(undefined); static EmptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
constructor(private context: ExtensionContext, public repoPath: string) { constructor(private context: ExtensionContext, public repoPath: string) {
super(() => this.dispose()); super(() => this.dispose());
@@ -139,7 +140,7 @@ export class GitService extends Disposable {
if (codeLensChanged) { if (codeLensChanged) {
Logger.log('CodeLens config changed; resetting CodeLens provider'); 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) { if (this._codeLensProvider) {
this._codeLensProvider.reset(); this._codeLensProvider.reset();
} }
@@ -154,7 +155,7 @@ export class GitService extends Disposable {
this._codeLensProvider = undefined; 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) { if (advancedChanged) {
@@ -166,6 +167,7 @@ export class GitService extends Disposable {
const disposables: Disposable[] = []; const disposables: Disposable[] = [];
disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed))); disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed)));
disposables.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved))); disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved)));
disposables.push(this._fsWatcher.onDidChange(this._onGitChanged, this)); disposables.push(this._fsWatcher.onDidChange(this._onGitChanged, this));
@@ -208,6 +210,22 @@ export class GitService extends Disposable {
this.config = cfg; this.config = cfg;
} }
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
if (!this.UseCaching) return;
if (e.document.uri.scheme !== DocumentSchemes.File) return;
// TODO: Rework this once https://github.com/Microsoft/vscode/issues/27231 is released in v1.13
// We have to defer because isDirty is not reliable inside this event
setTimeout(() => {
// If the document is dirty all is fine, we'll just wait for the save before clearing our cache
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 we should clear our cache for it
this._removeCachedEntry(e.document, RemoveCacheReason.DocumentSaved);
}, 1);
}
private _onGitChanged() { private _onGitChanged() {
this._gitCache.clear(); this._gitCache.clear();
@@ -219,7 +237,7 @@ export class GitService extends Disposable {
if (!this.UseCaching) return; if (!this.UseCaching) return;
if (document.uri.scheme !== DocumentSchemes.File) return; if (document.uri.scheme !== DocumentSchemes.File) return;
const cacheKey = this.getCacheEntryKey(document.fileName); const cacheKey = this.getCacheEntryKey(document.uri);
if (reason === RemoveCacheReason.DocumentSaved) { if (reason === RemoveCacheReason.DocumentSaved) {
// Don't remove broken blame on save (since otherwise we'll have to run the broken blame again) // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again)
@@ -269,18 +287,18 @@ export class GitService extends Disposable {
if (sha === undefined) { if (sha === undefined) {
// Get the most recent commit for this file name // Get the most recent commit for this file name
const c = await this.getLogCommit(repoPath, fileName); const c = await this.getLogCommit(repoPath, fileName);
if (!c) return undefined; if (c === undefined) return undefined;
sha = c.sha; sha = c.sha;
} }
// Get the full commit (so we can see if there are any matching renames in the file statuses) // Get the full commit (so we can see if there are any matching renames in the file statuses)
const log = await this.getLogForRepo(repoPath, sha, 1); const log = await this.getLogForRepo(repoPath, sha, 1);
if (!log) return undefined; if (log === undefined) return undefined;
const c = Iterables.first(log.commits.values()); const c = Iterables.first(log.commits.values());
const status = c.fileStatuses.find(_ => _.originalFileName === fileName); const status = c.fileStatuses.find(_ => _.originalFileName === fileName);
if (!status) return undefined; if (status === undefined) return undefined;
return status.fileName; return status.fileName;
} }
@@ -313,45 +331,30 @@ export class GitService extends Disposable {
public async getBlameability(uri: GitUri): Promise<boolean> { public async getBlameability(uri: GitUri): Promise<boolean> {
if (!this.UseCaching) return await this.isTracked(uri); if (!this.UseCaching) return await this.isTracked(uri);
const cacheKey = this.getCacheEntryKey(uri.fsPath); const cacheKey = this.getCacheEntryKey(uri);
const entry = this._gitCache.get(cacheKey); const entry = this._gitCache.get(cacheKey);
if (entry === undefined) return await this.isTracked(uri); if (entry === undefined) return await this.isTracked(uri);
return !entry.hasErrors; return !entry.hasErrors;
} }
async getBlameForFile(uri: GitUri): Promise<IGitBlame | undefined> { async getBlameForFile(uri: GitUri): Promise<GitBlame | undefined> {
let key = 'blame'; let key = 'blame';
if (uri.sha !== undefined) { if (uri.sha !== undefined) {
key += `:${uri.sha}`; key += `:${uri.sha}`;
} }
const fileName = uri.fsPath;
let entry: GitCacheEntry | undefined; let entry: GitCacheEntry | undefined;
if (this.UseCaching) { if (this.UseCaching) {
const cacheKey = this.getCacheEntryKey(fileName); const cacheKey = this.getCacheEntryKey(uri);
entry = this._gitCache.get(cacheKey); entry = this._gitCache.get(cacheKey);
if (entry !== undefined) { if (entry !== undefined) {
const cachedBlame = entry.get<ICachedBlame>(key); const cachedBlame = entry.get<CachedBlame>(key);
if (cachedBlame !== undefined) { if (cachedBlame !== undefined) {
Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
return cachedBlame.item; return cachedBlame.item;
} }
if (key !== 'blame') {
// Since we are looking for partial blame, see if we have the blame of the whole file
const cachedBlame = entry.get<ICachedBlame>('blame');
if (cachedBlame !== undefined) {
Logger.log(`? Cache(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
const blame = await cachedBlame.item;
if (blame !== undefined && blame.commits.has(uri.sha!)) {
Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
return cachedBlame.item;
}
}
}
} }
Logger.log(`Not Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); Logger.log(`Not Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
@@ -365,34 +368,35 @@ export class GitService extends Disposable {
Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`); Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
} }
const promise = this._getBlameForFile(uri, fileName, entry, key); const promise = this._getBlameForFile(uri, entry, key);
if (entry) { if (entry) {
Logger.log(`Add blame cache for '${entry.key}:${key}'`); Logger.log(`Add blame cache for '${entry.key}:${key}'`);
entry.set<ICachedBlame>(key, { entry.set<CachedBlame>(key, {
item: promise item: promise
} as ICachedBlame); } as CachedBlame);
} }
return promise; return promise;
} }
private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined, key: string): Promise<IGitBlame | undefined> { private async _getBlameForFile(uri: GitUri, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
const [file, root] = Git.splitPath(fileName, uri.repoPath, false); const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
const ignore = await this._gitignore; const ignore = await this._gitignore;
if (ignore && !ignore.filter([file]).length) { if (ignore && !ignore.filter([file]).length) {
Logger.log(`Skipping blame; '${fileName}' is gitignored`); Logger.log(`Skipping blame; '${uri.fsPath}' is gitignored`);
if (entry && entry.key) { if (entry && entry.key) {
this._onDidBlameFail.fire(entry.key); this._onDidBlameFail.fire(entry.key);
} }
return await GitService.EmptyPromise as IGitBlame; return await GitService.EmptyPromise as GitBlame;
} }
try { try {
const data = await Git.blame(root, file, uri.sha); const data = await Git.blame(root, file, uri.sha);
return GitBlameParser.parse(data, root, file); const blame = GitBlameParser.parse(data, root, file);
return blame;
} }
catch (ex) { catch (ex) {
// Trap and cache expected blame errors // Trap and cache expected blame errors
@@ -400,28 +404,31 @@ export class GitService extends Disposable {
const msg = ex && ex.toString(); const msg = ex && ex.toString();
Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`); Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedBlame>(key, { entry.set<CachedBlame>(key, {
item: GitService.EmptyPromise, item: GitService.EmptyPromise,
errorMessage: msg errorMessage: msg
} as ICachedBlame); } as CachedBlame);
this._onDidBlameFail.fire(entry.key); this._onDidBlameFail.fire(entry.key);
return await GitService.EmptyPromise as IGitBlame; return await GitService.EmptyPromise as GitBlame;
} }
return undefined; return undefined;
} }
} }
async getBlameForLine(uri: GitUri, line: number): Promise<IGitBlameLine | undefined> { async getBlameForLine(uri: GitUri, line: number): Promise<GitBlameLine | undefined> {
Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`); Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`);
if (this.UseCaching) { if (this.UseCaching) {
const blame = await this.getBlameForFile(uri); const blame = await this.getBlameForFile(uri);
if (blame === undefined) return undefined; if (blame === undefined) return undefined;
const blameLine = blame.lines[line]; let blameLine = blame.lines[line];
if (blameLine === undefined) return undefined; if (blameLine === undefined) {
if (blame.lines.length !== line) return undefined;
blameLine = blame.lines[line - 1];
}
const commit = blame.commits.get(blameLine.sha); const commit = blame.commits.get(blameLine.sha);
if (commit === undefined) return undefined; if (commit === undefined) return undefined;
@@ -430,7 +437,7 @@ export class GitService extends Disposable {
author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
commit: commit, commit: commit,
line: blameLine line: blameLine
} as IGitBlameLine; } as GitBlameLine;
} }
const fileName = uri.fsPath; const fileName = uri.fsPath;
@@ -438,7 +445,7 @@ export class GitService extends Disposable {
try { try {
const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1); const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1);
const blame = GitBlameParser.parse(data, uri.repoPath, fileName); const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
if (!blame) return undefined; if (blame === undefined) return undefined;
const commit = Iterables.first(blame.commits.values()); const commit = Iterables.first(blame.commits.values());
if (uri.repoPath) { if (uri.repoPath) {
@@ -448,41 +455,40 @@ export class GitService extends Disposable {
author: Iterables.first(blame.authors.values()), author: Iterables.first(blame.authors.values()),
commit: commit, commit: commit,
line: blame.lines[line] line: blame.lines[line]
} as IGitBlameLine; } as GitBlameLine;
} }
catch (ex) { catch (ex) {
return undefined; return undefined;
} }
} }
async getBlameForRange(uri: GitUri, range: Range): Promise<IGitBlameLines | undefined> { async getBlameForRange(uri: GitUri, range: Range): Promise<GitBlameLines | undefined> {
Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
const blame = await this.getBlameForFile(uri); const blame = await this.getBlameForFile(uri);
if (!blame) return undefined; if (blame === undefined) return undefined;
return this.getBlameForRangeSync(blame, uri, range); return this.getBlameForRangeSync(blame, uri, range);
} }
getBlameForRangeSync(blame: IGitBlame, uri: GitUri, range: Range): IGitBlameLines | undefined { getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); if (blame.lines.length === 0) return Object.assign({ allLines: blame.lines }, blame);
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
return Object.assign({ allLines: blame.lines }, blame); return Object.assign({ allLines: blame.lines }, blame);
} }
const lines = blame.lines.slice(range.start.line, range.end.line + 1); const lines = blame.lines.slice(range.start.line, range.end.line + 1);
const shas: Set<string> = new Set(); const shas = new Set(lines.map(l => l.sha));
lines.forEach(l => shas.add(l.sha));
const authors: Map<string, IGitAuthor> = new Map(); const authors: Map<string, GitAuthor> = new Map();
const commits: Map<string, GitCommit> = new Map(); const commits: Map<string, GitBlameCommit> = new Map();
blame.commits.forEach(c => { for (const c of blame.commits.values()) {
if (!shas.has(c.sha)) return; if (!shas.has(c.sha)) return;
const commit: GitCommit = new GitCommit('blame', c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, const commit = new GitBlameCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message,
c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName); c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName);
commits.set(c.sha, commit); commits.set(c.sha, commit);
@@ -496,26 +502,23 @@ export class GitService extends Disposable {
} }
author.lineCount += commit.lines.length; author.lineCount += commit.lines.length;
}); }
const sortedAuthors: Map<string, IGitAuthor> = new Map(); const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount));
Array.from(authors.values())
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(a => sortedAuthors.set(a.name, a));
return { return {
authors: sortedAuthors, authors: sortedAuthors,
commits: commits, commits: commits,
lines: lines, lines: lines,
allLines: blame.lines allLines: blame.lines
} as IGitBlameLines; } as GitBlameLines;
} }
async getBlameLocations(uri: GitUri, range: Range, selectedSha?: string, line?: number): Promise<Location[] | undefined> { async getBlameLocations(uri: GitUri, range: Range, selectedSha?: string, line?: number): Promise<Location[] | undefined> {
Logger.log(`getBlameLocations('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`); Logger.log(`getBlameLocations('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
const blame = await this.getBlameForRange(uri, range); const blame = await this.getBlameForRange(uri, range);
if (!blame) return undefined; if (blame === undefined) return undefined;
const commitCount = blame.commits.size; const commitCount = blame.commits.size;
@@ -550,8 +553,10 @@ export class GitService extends Disposable {
return branches; return branches;
} }
getCacheEntryKey(fileName: string) { getCacheEntryKey(fileName: string): string;
return Git.normalizePath(fileName).toLowerCase(); getCacheEntryKey(uri: Uri): string;
getCacheEntryKey(fileNameOrUri: string | Uri): string {
return Git.normalizePath(typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.fsPath).toLowerCase();
} }
async getConfig(key: string, repoPath?: string): Promise<string> { async getConfig(key: string, repoPath?: string): Promise<string> {
@@ -560,13 +565,17 @@ export class GitService extends Disposable {
return await Git.config_get(key, repoPath); return await Git.config_get(key, repoPath);
} }
getGitUriForFile(fileName: string) { getGitUriForFile(uri: Uri) {
const cacheKey = this.getCacheEntryKey(fileName); const cacheKey = this.getCacheEntryKey(uri);
const entry = this._uriCache.get(cacheKey); const entry = this._uriCache.get(cacheKey);
return entry && entry.uri; return entry && entry.uri;
} }
async getDiffForFile(repoPath: string | undefined, fileName: string, sha1?: string, sha2?: string): Promise<IGitDiff | undefined> { async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise<GitDiff | undefined> {
if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) {
sha2 = uri.sha;
}
let key = 'diff'; let key = 'diff';
if (sha1 !== undefined) { if (sha1 !== undefined) {
key += `:${sha1}`; key += `:${sha1}`;
@@ -577,18 +586,18 @@ export class GitService extends Disposable {
let entry: GitCacheEntry | undefined; let entry: GitCacheEntry | undefined;
if (this.UseCaching) { if (this.UseCaching) {
const cacheKey = this.getCacheEntryKey(fileName); const cacheKey = this.getCacheEntryKey(uri);
entry = this._gitCache.get(cacheKey); entry = this._gitCache.get(cacheKey);
if (entry !== undefined) { if (entry !== undefined) {
const cachedDiff = entry.get<ICachedDiff>(key); const cachedDiff = entry.get<CachedDiff>(key);
if (cachedDiff !== undefined) { if (cachedDiff !== undefined) {
Logger.log(`Cached(${key}): getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); Logger.log(`Cached(${key}): getDiffForFile('${uri.repoPath}', '${uri.fsPath}', ${sha1}, ${sha2})`);
return cachedDiff.item; return cachedDiff.item;
} }
} }
Logger.log(`Not Cached(${key}): getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); Logger.log(`Not Cached(${key}): getDiffForFile('${uri.repoPath}', '${uri.fsPath}', ${sha1}, ${sha2})`);
if (entry === undefined) { if (entry === undefined) {
entry = new GitCacheEntry(cacheKey); entry = new GitCacheEntry(cacheKey);
@@ -596,28 +605,29 @@ export class GitService extends Disposable {
} }
} }
else { else {
Logger.log(`getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', ${sha1}, ${sha2})`);
} }
const promise = this._getDiffForFile(repoPath, fileName, sha1, sha2, entry, key); const promise = this._getDiffForFile(uri.repoPath, uri.fsPath, sha1, sha2, entry, key);
if (entry) { if (entry) {
Logger.log(`Add log cache for '${entry.key}:${key}'`); Logger.log(`Add log cache for '${entry.key}:${key}'`);
entry.set<ICachedDiff>(key, { entry.set<CachedDiff>(key, {
item: promise item: promise
} as ICachedDiff); } as CachedDiff);
} }
return promise; return promise;
} }
private async _getDiffForFile(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, entry: GitCacheEntry | undefined, key: string): Promise<IGitDiff | undefined> { private async _getDiffForFile(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, entry: GitCacheEntry | undefined, key: string): Promise<GitDiff | undefined> {
const [file, root] = Git.splitPath(fileName, repoPath, false); const [file, root] = Git.splitPath(fileName, repoPath, false);
try { try {
const data = await Git.diff(root, file, sha1, sha2); const data = await Git.diff(root, file, sha1, sha2);
return GitDiffParser.parse(data, this.config.debug); const diff = GitDiffParser.parse(data);
return diff;
} }
catch (ex) { catch (ex) {
// Trap and cache expected diff errors // Trap and cache expected diff errors
@@ -625,33 +635,50 @@ export class GitService extends Disposable {
const msg = ex && ex.toString(); const msg = ex && ex.toString();
Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`); Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedDiff>(key, { entry.set<CachedDiff>(key, {
item: GitService.EmptyPromise, item: GitService.EmptyPromise,
errorMessage: msg errorMessage: msg
} as ICachedDiff); } as CachedDiff);
return await GitService.EmptyPromise as IGitDiff; return await GitService.EmptyPromise as GitDiff;
} }
return undefined; return undefined;
} }
} }
async getDiffForLine(repoPath: string | undefined, fileName: string, line: number, sha1?: string, sha2?: string): Promise<[string | undefined, string | undefined] | undefined> { async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<[GitDiffLine | undefined, GitDiffLine | undefined]> {
try { try {
const diff = await this.getDiffForFile(repoPath, fileName, sha1, sha2); const diff = await this.getDiffForFile(uri, sha1, sha2);
if (diff === undefined) return undefined; if (diff === undefined) return [undefined, undefined];
const chunk = diff.chunks.find(_ => Math.min(_.originalStart, _.changesStart) <= line && Math.max(_.originalEnd, _.changesEnd) >= line); const chunk = diff.chunks.find(_ => _.currentPosition.start <= line && _.currentPosition.end >= 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
line = line - chunk.currentPosition.start + 1;
let count = 0;
let deleted = 0;
for (const l of chunk.current) {
if (l === undefined) {
deleted++;
if (count === line) break;
continue;
}
if (count === line) break;
count++;
}
return [ return [
chunk.original[line - chunk.originalStart + 1], chunk.previous[line + deleted - 1],
chunk.changes[line - chunk.changesStart + 1] chunk.current[line + deleted + (chunk.currentPosition.start - chunk.previousPosition.start)]
]; ];
} }
catch (ex) { catch (ex) {
return undefined; return [undefined, undefined];
} }
} }
@@ -662,22 +689,22 @@ export class GitService extends Disposable {
if (typeof shaOrOptions === 'string') { if (typeof shaOrOptions === 'string') {
sha = shaOrOptions; sha = shaOrOptions;
} }
else if (!options) { else if (options === undefined) {
options = shaOrOptions; options = shaOrOptions;
} }
options = options || {}; options = options || {};
const log = await this.getLogForFile(repoPath, fileName, sha, options.previous ? 2 : 1); const log = await this.getLogForFile(repoPath, fileName, sha, options.previous ? 2 : 1);
if (!log) return undefined; if (log === undefined) return undefined;
const commit = sha && log.commits.get(sha); const commit = sha && log.commits.get(sha);
if (!commit && sha && !options.firstIfMissing) return undefined; if (commit === undefined && sha && !options.firstIfMissing) return undefined;
return commit || Iterables.first(log.commits.values()); return commit || Iterables.first(log.commits.values());
} }
async getLogForRepo(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false): Promise<IGitLog | undefined> { async getLogForRepo(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false): Promise<GitLog | undefined> {
Logger.log(`getLogForRepo('${repoPath}', ${sha}, ${maxCount})`); Logger.log(`getLogForRepo('${repoPath}', ${sha}, ${maxCount})`);
if (maxCount == null) { if (maxCount == null) {
@@ -686,14 +713,15 @@ export class GitService extends Disposable {
try { try {
const data = await Git.log(repoPath, sha, maxCount, reverse); const data = await Git.log(repoPath, sha, maxCount, reverse);
return GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined); const log = GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined);
return log;
} }
catch (ex) { catch (ex) {
return undefined; return undefined;
} }
} }
async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, maxCount?: number): Promise<IGitLog | undefined> { async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, maxCount?: number): Promise<GitLog | undefined> {
Logger.log(`getLogForRepoSearch('${repoPath}', ${search}, ${searchBy}, ${maxCount})`); Logger.log(`getLogForRepoSearch('${repoPath}', ${search}, ${searchBy}, ${maxCount})`);
if (maxCount == null) { if (maxCount == null) {
@@ -719,14 +747,15 @@ export class GitService extends Disposable {
try { try {
const data = await Git.log_search(repoPath, searchArgs, maxCount); const data = await Git.log_search(repoPath, searchArgs, maxCount);
return GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); const log = GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined);
return log;
} }
catch (ex) { catch (ex) {
return undefined; return undefined;
} }
} }
async getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise<IGitLog | undefined> { async getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise<GitLog | undefined> {
let key = 'log'; let key = 'log';
if (sha !== undefined) { if (sha !== undefined) {
key += `:${sha}`; key += `:${sha}`;
@@ -741,7 +770,7 @@ export class GitService extends Disposable {
entry = this._gitCache.get(cacheKey); entry = this._gitCache.get(cacheKey);
if (entry !== undefined) { if (entry !== undefined) {
const cachedLog = entry.get<ICachedLog>(key); const cachedLog = entry.get<CachedLog>(key);
if (cachedLog !== undefined) { if (cachedLog !== undefined) {
Logger.log(`Cached(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); Logger.log(`Cached(${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`);
return cachedLog.item; return cachedLog.item;
@@ -749,7 +778,7 @@ export class GitService extends Disposable {
if (key !== 'log') { if (key !== 'log') {
// Since we are looking for partial log, see if we have the log of the whole file // Since we are looking for partial log, see if we have the log of the whole file
const cachedLog = entry.get<ICachedLog>('log'); const cachedLog = entry.get<CachedLog>('log');
if (cachedLog !== undefined) { if (cachedLog !== undefined) {
if (sha === undefined) { if (sha === undefined) {
Logger.log(`Cached(~${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`); Logger.log(`Cached(~${key}): getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, undefined, false)`);
@@ -782,26 +811,27 @@ export class GitService extends Disposable {
if (entry) { if (entry) {
Logger.log(`Add log cache for '${entry.key}:${key}'`); Logger.log(`Add log cache for '${entry.key}:${key}'`);
entry.set<ICachedLog>(key, { entry.set<CachedLog>(key, {
item: promise item: promise
} as ICachedLog); } as CachedLog);
} }
return promise; return promise;
} }
private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined, key: string): Promise<IGitLog | undefined> { private async _getLogForFile(repoPath: string | undefined, fileName: string, sha: string | undefined, range: Range | undefined, maxCount: number | undefined, reverse: boolean, entry: GitCacheEntry | undefined, key: string): Promise<GitLog | undefined> {
const [file, root] = Git.splitPath(fileName, repoPath, false); const [file, root] = Git.splitPath(fileName, repoPath, false);
const ignore = await this._gitignore; const ignore = await this._gitignore;
if (ignore && !ignore.filter([file]).length) { if (ignore && !ignore.filter([file]).length) {
Logger.log(`Skipping log; '${fileName}' is gitignored`); Logger.log(`Skipping log; '${fileName}' is gitignored`);
return await GitService.EmptyPromise as IGitLog; return await GitService.EmptyPromise as GitLog;
} }
try { try {
const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1); const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1);
return GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range); const log = GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range);
return log;
} }
catch (ex) { catch (ex) {
// Trap and cache expected log errors // Trap and cache expected log errors
@@ -809,12 +839,12 @@ export class GitService extends Disposable {
const msg = ex && ex.toString(); const msg = ex && ex.toString();
Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`); Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedLog>(key, { entry.set<CachedLog>(key, {
item: GitService.EmptyPromise, item: GitService.EmptyPromise,
errorMessage: msg errorMessage: msg
} as ICachedLog); } as CachedLog);
return await GitService.EmptyPromise as IGitLog; return await GitService.EmptyPromise as GitLog;
} }
return undefined; return undefined;
@@ -825,7 +855,7 @@ export class GitService extends Disposable {
Logger.log(`getLogLocations('${uri.repoPath}', '${uri.fsPath}', ${uri.sha}, ${selectedSha}, ${line})`); Logger.log(`getLogLocations('${uri.repoPath}', '${uri.fsPath}', ${uri.sha}, ${selectedSha}, ${line})`);
const log = await this.getLogForFile(uri.repoPath, uri.fsPath, uri.sha); const log = await this.getLogForFile(uri.repoPath, uri.fsPath, uri.sha);
if (!log) return undefined; if (log === undefined) return undefined;
const commitCount = log.commits.size; const commitCount = log.commits.size;
@@ -845,7 +875,6 @@ export class GitService extends Disposable {
} }
async getRemotes(repoPath: string): Promise<GitRemote[]> { async getRemotes(repoPath: string): Promise<GitRemote[]> {
if (!this.config.insiders) return [];
if (!repoPath) return []; if (!repoPath) return [];
Logger.log(`getRemotes('${repoPath}')`); Logger.log(`getRemotes('${repoPath}')`);
@@ -883,11 +912,12 @@ export class GitService extends Disposable {
return repoPath; return repoPath;
} }
async getStashList(repoPath: string): Promise<IGitStash | undefined> { async getStashList(repoPath: string): Promise<GitStash | undefined> {
Logger.log(`getStash('${repoPath}')`); Logger.log(`getStash('${repoPath}')`);
const data = await Git.stash_list(repoPath); const data = await Git.stash_list(repoPath);
return GitStashParser.parse(data, repoPath); const stash = GitStashParser.parse(data, repoPath);
return stash;
} }
async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> { async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> {
@@ -902,13 +932,14 @@ export class GitService extends Disposable {
return status.files[0]; return status.files[0];
} }
async getStatusForRepo(repoPath: string): Promise<IGitStatus | undefined> { async getStatusForRepo(repoPath: string): Promise<GitStatus | undefined> {
Logger.log(`getStatusForRepo('${repoPath}')`); Logger.log(`getStatusForRepo('${repoPath}')`);
const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
const data = await Git.status(repoPath, porcelainVersion); const data = await Git.status(repoPath, porcelainVersion);
return GitStatusParser.parse(data, repoPath, porcelainVersion); const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
return status;
} }
async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string) { async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string) {
@@ -927,19 +958,10 @@ export class GitService extends Disposable {
return Git.show(repoPath, fileName, sha); return Git.show(repoPath, fileName, sha);
} }
hasGitUriForFile(editor: TextEditor): boolean; hasGitUriForFile(editor: TextEditor): boolean {
hasGitUriForFile(fileName: string): boolean; if (editor === undefined || editor.document === undefined || editor.document.uri === undefined) return false;
hasGitUriForFile(fileNameOrEditor: string | TextEditor): boolean {
let fileName: string;
if (typeof fileNameOrEditor === 'string') {
fileName = fileNameOrEditor;
}
else {
if (!fileNameOrEditor || !fileNameOrEditor.document || !fileNameOrEditor.document.uri) return false;
fileName = fileNameOrEditor.document.uri.fsPath;
}
const cacheKey = this.getCacheEntryKey(fileName); const cacheKey = this.getCacheEntryKey(editor.document.uri);
return this._uriCache.has(cacheKey); return this._uriCache.has(cacheKey);
} }
@@ -994,8 +1016,7 @@ export class GitService extends Disposable {
} }
toggleCodeLens(editor: TextEditor) { toggleCodeLens(editor: TextEditor) {
if (this.config.codeLens.visibility === CodeLensVisibility.Off || if (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled) return;
(!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return;
Logger.log(`toggleCodeLens()`); Logger.log(`toggleCodeLens()`);
if (this._codeLensProviderDisposable) { if (this._codeLensProviderDisposable) {

103
src/messages.ts Normal file
View File

@@ -0,0 +1,103 @@
'use strict';
import { commands, ExtensionContext, Uri, window } from 'vscode';
import { BuiltInCommands } from './constants';
import { GitCommit } from './gitService';
import * as moment from 'moment';
export type SuppressedKeys = 'suppressCommitHasNoPreviousCommitWarning' |
'suppressCommitNotFoundWarning' |
'suppressFileNotUnderSourceControlWarning' |
'suppressGitVersionWarning' |
'suppressLineUncommittedWarning' |
'suppressNoRepositoryWarning' |
'suppressUpdateNotice';
export const SuppressedKeys = {
CommitHasNoPreviousCommitWarning: 'suppressCommitHasNoPreviousCommitWarning' as SuppressedKeys,
CommitNotFoundWarning: 'suppressCommitNotFoundWarning' as SuppressedKeys,
FileNotUnderSourceControlWarning: 'suppressFileNotUnderSourceControlWarning' as SuppressedKeys,
GitVersionWarning: 'suppressGitVersionWarning' as SuppressedKeys,
LineUncommittedWarning: 'suppressLineUncommittedWarning' as SuppressedKeys,
NoRepositoryWarning: 'suppressNoRepositoryWarning' as SuppressedKeys,
UpdateNotice: 'suppressUpdateNotice' as SuppressedKeys
};
export class Messages {
static context: ExtensionContext;
static configure(context: ExtensionContext) {
this.context = context;
}
static showCommitHasNoPreviousCommitWarningMessage(commit: GitCommit): Promise<string | undefined> {
return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${moment(commit.date).fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
}
static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> {
return Messages._showMessage('warn', `${message}. The commit could not be found`, SuppressedKeys.CommitNotFoundWarning);
}
static showFileNotUnderSourceControlWarningMessage(message: string): Promise<string | undefined> {
return Messages._showMessage('warn', `${message}. The file is probably not under source control`, SuppressedKeys.FileNotUnderSourceControlWarning);
}
static showLineUncommittedWarningMessage(message: string): Promise<string | undefined> {
return Messages._showMessage('warn', `${message}. The line has uncommitted changes`, SuppressedKeys.LineUncommittedWarning);
}
static showNoRepositoryWarningMessage(message: string): Promise<string | undefined> {
return Messages._showMessage('warn', `${message}. No repository could be found`, SuppressedKeys.NoRepositoryWarning);
}
static showUnsupportedGitVersionErrorMessage(version: string): Promise<string | undefined> {
return Messages._showMessage('error', `GitLens requires a newer version of Git (>= 2.2.0) than is currently installed (${version}). Please install a more recent version of Git.`, SuppressedKeys.GitVersionWarning);
}
static async showUpdateMessage(version: string): Promise<string | undefined> {
const viewReleaseNotes = 'View Release Notes';
const result = await Messages._showMessage('info', `GitLens has been updated to v${version}`, SuppressedKeys.UpdateNotice, undefined, viewReleaseNotes);
if (result === viewReleaseNotes) {
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens/changelog'));
}
return result;
}
static async showWelcomeMessage(): Promise<string | undefined> {
const viewDocs = 'View Docs';
const result = await window.showInformationMessage(`Thank you for choosing GitLens! GitLens is powerful, feature rich, and highly configurable, so please be sure to view the docs and tailor it to suit your needs.`, viewDocs);
if (result === viewDocs) {
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens'));
}
return result;
}
private static async _showMessage(type: 'info' | 'warn' | 'error', message: string, suppressionKey: SuppressedKeys, dontShowAgain: string | null = 'Don\'t Show Again', ...actions: any[]): Promise<string | undefined> {
if (Messages.context.globalState.get(suppressionKey, false)) return undefined;
if (dontShowAgain !== null) {
actions.push(dontShowAgain);
}
let result: string | undefined = undefined;
switch (type) {
case 'info':
result = await window.showInformationMessage(message, ...actions);
break;
case 'warn':
result = await window.showWarningMessage(message, ...actions);
break;
case 'error':
result = await window.showErrorMessage(message, ...actions);
break;
}
if (dontShowAgain === null || result === dontShowAgain) {
await Messages.context.globalState.update(suppressionKey, true);
return undefined;
}
return result;
}
}

View File

@@ -3,7 +3,7 @@ import { Arrays, Iterables } from '../system';
import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, Keyboard, KeyNoopCommand, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands'; import { Commands, Keyboard, KeyNoopCommand, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common';
import { GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { GitLog, GitService, GitUri, RemoteResource } from '../gitService';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
export class BranchHistoryQuickPick { export class BranchHistoryQuickPick {
@@ -17,7 +17,7 @@ export class BranchHistoryQuickPick {
}); });
} }
static async show(git: GitService, log: IGitLog, uri: GitUri | undefined, branch: string, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> { static async show(git: GitService, log: GitLog, uri: GitUri | undefined, branch: string, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[]; const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[];
const currentCommand = new CommandQuickPickItem({ const currentCommand = new CommandQuickPickItem({

View File

@@ -3,7 +3,7 @@ import { Arrays, Iterables } from '../system';
import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode'; import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode';
import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, DiffWithPreviousCommandArgs, Keyboard, KeyNoopCommand, Keys, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandCommandArgs, DiffWithPreviousCommandArgs, Keyboard, KeyNoopCommand, Keys, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem } from './common';
import { getGitStatusIcon, GitCommit, GitLogCommit, GitService, GitStashCommit, GitStatusFileStatus, GitUri, IGitCommitInfo, IGitLog, IGitStatusFile, RemoteResource } from '../gitService'; import { getGitStatusIcon, GitCommit, GitLog, GitLogCommit, GitService, GitStashCommit, GitStatusFileStatus, GitUri, IGitCommitInfo, IGitStatusFile, RemoteResource } from '../gitService';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment'; import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
@@ -103,14 +103,14 @@ export class OpenCommitWorkingTreeFilesCommandQuickPickItem extends OpenFilesCom
export class CommitDetailsQuickPick { export class CommitDetailsQuickPick {
static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, repoLog?: IGitLog): Promise<CommitWithFileStatusQuickPickItem | CommandQuickPickItem | undefined> { static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, repoLog?: GitLog): Promise<CommitWithFileStatusQuickPickItem | CommandQuickPickItem | undefined> {
const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.fileStatuses.map(fs => new CommitWithFileStatusQuickPickItem(commit, fs)); const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.fileStatuses.map(fs => new CommitWithFileStatusQuickPickItem(commit, fs));
const stash = commit.type === 'stash'; const stash = commit.type === 'stash';
let index = 0; let index = 0;
if (stash && git.config.insiders) { if (stash) {
items.splice(index++, 0, new CommandQuickPickItem({ items.splice(index++, 0, new CommandQuickPickItem({
label: `$(git-pull-request) Apply Stashed Changes`, label: `$(git-pull-request) Apply Stashed Changes`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}` description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`

View File

@@ -3,7 +3,7 @@ import { Arrays, Iterables } from '../system';
import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, Keyboard, KeyNoopCommand, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, Keyboard, KeyNoopCommand, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common';
import { GitBranch, GitLogCommit, GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { GitBranch, GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment'; import * as moment from 'moment';
import * as path from 'path'; import * as path from 'path';
@@ -41,7 +41,7 @@ export class OpenCommitWorkingTreeFileCommandQuickPickItem extends OpenFileComma
export class CommitFileDetailsQuickPick { export class CommitFileDetailsQuickPick {
static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, fileLog?: IGitLog): Promise<CommandQuickPickItem | undefined> { static async show(git: GitService, commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, fileLog?: GitLog): Promise<CommandQuickPickItem | undefined> {
const items: CommandQuickPickItem[] = []; const items: CommandQuickPickItem[] = [];
const stash = commit.type === 'stash'; const stash = commit.type === 'stash';
@@ -72,7 +72,7 @@ export class CommitFileDetailsQuickPick {
if (commit.previousSha) { if (commit.previousSha) {
items.push(new CommandQuickPickItem({ items.push(new CommandQuickPickItem({
label: `$(git-compare) Compare with Previous Commit`, label: `$(git-compare) Compare File with Previous`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}` description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.previousShortSha} \u00a0 $(git-compare) \u00a0 $(git-commit) ${commit.shortSha}`
}, Commands.DiffWithPrevious, [ }, Commands.DiffWithPrevious, [
commit.uri, commit.uri,
@@ -85,7 +85,7 @@ export class CommitFileDetailsQuickPick {
if (commit.workingFileName) { if (commit.workingFileName) {
items.push(new CommandQuickPickItem({ items.push(new CommandQuickPickItem({
label: `$(git-compare) Compare with Working Tree`, label: `$(git-compare) Compare File with Working Tree`,
description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha} \u00a0 $(git-compare) \u00a0 $(file-text) ${workingName}` description: `\u00a0 \u2014 \u00a0\u00a0 $(git-commit) ${commit.shortSha} \u00a0 $(git-compare) \u00a0 $(file-text) ${workingName}`
}, Commands.DiffWithWorking, [ }, Commands.DiffWithWorking, [
Uri.file(path.resolve(commit.repoPath, commit.workingFileName)), Uri.file(path.resolve(commit.repoPath, commit.workingFileName)),

View File

@@ -2,12 +2,12 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { QuickPickOptions, window } from 'vscode'; import { QuickPickOptions, window } from 'vscode';
import { Keyboard } from '../commands'; import { Keyboard } from '../commands';
import { GitService, IGitLog } from '../gitService'; import { GitLog, GitService } from '../gitService';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks';
export class CommitsQuickPick { export class CommitsQuickPick {
static async show(git: GitService, log: IGitLog, placeHolder: string, goBackCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> { static async show(git: GitService, log: GitLog, placeHolder: string, goBackCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = ((log && Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[]; const items = ((log && Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[];
if (goBackCommand) { if (goBackCommand) {

View File

@@ -1,8 +1,7 @@
'use strict'; 'use strict';
import { CancellationTokenSource, commands, Disposable, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, TextEditor, Uri, window, workspace } from 'vscode'; import { CancellationTokenSource, commands, Disposable, QuickPickItem, QuickPickOptions, TextDocumentShowOptions, TextEditor, Uri, window, workspace } from 'vscode';
import { Commands, Keyboard, KeyboardScope, KeyMapping, Keys, openEditor } from '../commands'; import { Commands, Keyboard, KeyboardScope, KeyMapping, Keys, openEditor } from '../commands';
import { IAdvancedConfig } from '../configuration'; import { ExtensionKey, IAdvancedConfig } from '../configuration';
import { ExtensionKey } from '../constants';
import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService'; import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService';
// import { Logger } from '../logger'; // import { Logger } from '../logger';
import * as moment from 'moment'; import * as moment from 'moment';

View File

@@ -3,7 +3,7 @@ import { Arrays, Iterables } from '../system';
import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode';
import { Commands, Keyboard, KeyNoopCommand, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { Commands, Keyboard, KeyNoopCommand, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './common';
import { GitService, GitUri, IGitLog, RemoteResource } from '../gitService'; import { GitLog, GitService, GitUri, RemoteResource } from '../gitService';
import { OpenRemotesCommandQuickPickItem } from './remotes'; import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as path from 'path'; import * as path from 'path';
@@ -18,7 +18,7 @@ export class FileHistoryQuickPick {
}); });
} }
static async show(git: GitService, log: IGitLog, uri: GitUri, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> { static async show(git: GitService, log: GitLog, uri: GitUri, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem, nextPageCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[]; const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[];
let previousPageCommand: CommandQuickPickItem | undefined = undefined; let previousPageCommand: CommandQuickPickItem | undefined = undefined;

View File

@@ -3,7 +3,7 @@ import { Iterables } from '../system';
import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode'; import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode';
import { Commands, DiffWithWorkingCommandArgs, Keyboard, Keys, OpenChangedFilesCommandArgs, ShowQuickBranchHistoryCommandArgs, ShowQuickRepoStatusCommandArgs, ShowQuickStashListCommandArgs } from '../commands'; import { Commands, DiffWithWorkingCommandArgs, Keyboard, Keys, OpenChangedFilesCommandArgs, ShowQuickBranchHistoryCommandArgs, ShowQuickRepoStatusCommandArgs, ShowQuickStashListCommandArgs } from '../commands';
import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, OpenFileCommandQuickPickItem, QuickPickItem } from './common'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, OpenFileCommandQuickPickItem, QuickPickItem } from './common';
import { GitService, GitStatusFile, GitUri, IGitStatus } from '../gitService'; import { GitService, GitStatus, GitStatusFile, GitUri } from '../gitService';
import * as path from 'path'; import * as path from 'path';
export class OpenStatusFileCommandQuickPickItem extends OpenFileCommandQuickPickItem { export class OpenStatusFileCommandQuickPickItem extends OpenFileCommandQuickPickItem {
@@ -58,7 +58,7 @@ export class OpenStatusFilesCommandQuickPickItem extends CommandQuickPickItem {
export class RepoStatusQuickPick { export class RepoStatusQuickPick {
static async show(status: IGitStatus, goBackCommand?: CommandQuickPickItem): Promise<OpenStatusFileCommandQuickPickItem | OpenStatusFilesCommandQuickPickItem | CommandQuickPickItem | undefined> { static async show(status: GitStatus, goBackCommand?: CommandQuickPickItem): Promise<OpenStatusFileCommandQuickPickItem | OpenStatusFilesCommandQuickPickItem | CommandQuickPickItem | undefined> {
// Sort the status by staged and then filename // Sort the status by staged and then filename
const files = status.files; const files = status.files;
files.sort((a, b) => (a.staged ? -1 : 1) - (b.staged ? -1 : 1) || a.fileName.localeCompare(b.fileName)); files.sort((a, b) => (a.staged ? -1 : 1) - (b.staged ? -1 : 1) || a.fileName.localeCompare(b.fileName));

View File

@@ -2,15 +2,15 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { QuickPickOptions, window } from 'vscode'; import { QuickPickOptions, window } from 'vscode';
import { Commands, Keyboard, StashSaveCommandArgs } from '../commands'; import { Commands, Keyboard, StashSaveCommandArgs } from '../commands';
import { GitService, IGitStash } from '../gitService'; import { GitService, GitStash } from '../gitService';
import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks'; import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks';
export class StashListQuickPick { export class StashListQuickPick {
static async show(git: GitService, stash: IGitStash, mode: 'list' | 'apply', goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> { static async show(git: GitService, stash: GitStash, mode: 'list' | 'apply', goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = ((stash && Array.from(Iterables.map(stash.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[]; const items = ((stash && Array.from(Iterables.map(stash.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[];
if (mode === 'list' && git.config.insiders) { if (mode === 'list') {
items.splice(0, 0, new CommandQuickPickItem({ items.splice(0, 0, new CommandQuickPickItem({
label: `$(repo-push) Stash Unstaged Changes`, label: `$(repo-push) Stash Unstaged Changes`,
description: `\u00a0 \u2014 \u00a0\u00a0 stashes only unstaged changes` description: `\u00a0 \u2014 \u00a0\u00a0 stashes only unstaged changes`

View File

@@ -15,4 +15,8 @@ export namespace Functions {
export function once<T extends Function>(fn: T): T { export function once<T extends Function>(fn: T): T {
return _once(fn); return _once(fn);
} }
export async function wait(ms: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}
} }

View File

@@ -92,7 +92,7 @@ export namespace Iterables {
return source.next().value; return source.next().value;
} }
export function* skip<T>(source: Iterable<T> | IterableIterator<T>, count: number): Iterable<T> { export function* skip<T>(source: Iterable<T> | IterableIterator<T>, count: number): Iterable<T> | IterableIterator<T> {
let i = 0; let i = 0;
for (const item of source) { for (const item of source) {
if (i >= count) yield item; if (i >= count) yield item;

View File

@@ -55,4 +55,10 @@ export namespace Objects {
} }
} }
} }
export function* values<T>(o: any): IterableIterator<T> {
for (const key in o) {
yield o[key];
}
}
} }

View File

@@ -5,4 +5,90 @@ export namespace Strings {
export function escapeRegExp(s: string): string { export function escapeRegExp(s: string): string {
return _escapeRegExp(s); 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, context: object): string {
if (!template) return template;
template = template.replace(TokenSanitizeRegex, '$${this.$1}');
return new Function(`return \`${template}\`;`).call(context);
}
export function* lines(s: string): IterableIterator<string> {
let i = 0;
while (i < s.length) {
let j = s.indexOf('\n', i);
if (j === -1) {
j = s.length;
}
yield s.substring(i, j);
i = j + 1;
}
}
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`;
}
} }

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
import { Disposable, env, version, workspace } from 'vscode'; import { Disposable, env, version, workspace } from 'vscode';
import { ExtensionKey, IConfig } from './configuration';
import * as os from 'os'; import * as os from 'os';
let _reporter: TelemetryReporter; let _reporter: TelemetryReporter;
@@ -7,21 +8,28 @@ let _reporter: TelemetryReporter;
export class Telemetry extends Disposable { export class Telemetry extends Disposable {
static configure(key: string) { static configure(key: string) {
if (!workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) return; const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
if (!cfg.advanced.telemetry.enabled || !workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) return;
_reporter = new TelemetryReporter(key); _reporter = new TelemetryReporter(key);
} }
static setContext(context?: { [key: string]: string }) { static setContext(context?: { [key: string]: string }) {
_reporter && _reporter.setContext(context); if (_reporter === undefined) return;
_reporter.setContext(context);
} }
static trackEvent(name: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number; }) { static trackEvent(name: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number; }) {
_reporter && _reporter.trackEvent(name, properties, measurements); if (_reporter === undefined) return;
_reporter.trackEvent(name, properties, measurements);
} }
static trackException(ex: Error) { static trackException(ex: Error) {
_reporter && _reporter.trackException(ex); if (_reporter === undefined) return;
_reporter.trackException(ex);
} }
} }
@@ -44,11 +52,12 @@ export class TelemetryReporter {
} }
else { else {
this._client = this.appInsights.setup(key) this._client = this.appInsights.setup(key)
.setAutoCollectConsole(false)
.setAutoCollectDependencies(false)
.setAutoCollectExceptions(false)
.setAutoCollectPerformance(false)
.setAutoCollectRequests(false) .setAutoCollectRequests(false)
.setAutoCollectPerformance(false)
.setAutoCollectExceptions(false)
.setAutoCollectDependencies(false)
.setAutoCollectConsole(false)
.setAutoDependencyCorrelation(false)
.setOfflineMode(true) .setOfflineMode(true)
.start() .start()
.client; .client;

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"lib": [ "es2015" ], "lib": [ "es2015", "es2016" ],
"module": "commonjs", "module": "commonjs",
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitReturns": true, "noImplicitReturns": true,
@@ -12,7 +12,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"target": "es2015" "target": "es2016"
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",

Some files were not shown because too many files have changed in this diff Show More