Compare commits
60 Commits
v3.5.0-bet
...
v4.0.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c7171d7f | ||
|
|
dd0b95498e | ||
|
|
e6316400f0 | ||
|
|
eeff31cf27 | ||
|
|
eb3b9ad6c9 | ||
|
|
10674124c8 | ||
|
|
e0f66247cf | ||
|
|
badd999db1 | ||
|
|
9ae4cc36a1 | ||
|
|
30bb4398a3 | ||
|
|
9c7a971e21 | ||
|
|
ba59fb29ad | ||
|
|
50ba3e1446 | ||
|
|
62e5ef6225 | ||
|
|
ed54d289dd | ||
|
|
2a8dafd9e9 | ||
|
|
016c561ead | ||
|
|
9cf86a41ec | ||
|
|
4eb1c3e36a | ||
|
|
95e0a6c71b | ||
|
|
35ca8106c9 | ||
|
|
948a75de79 | ||
|
|
4b0891b949 | ||
|
|
a9d94868e7 | ||
|
|
3c45c7e049 | ||
|
|
ba0d55d5d4 | ||
|
|
d2dc172042 | ||
|
|
e5e582d300 | ||
|
|
7c9e4b911c | ||
|
|
42fdf9f327 | ||
|
|
547d50fed6 | ||
|
|
6c33686335 | ||
|
|
28355d41b6 | ||
|
|
6e5bb2343e | ||
|
|
d01c592533 | ||
|
|
37e48ded2d | ||
|
|
e3e7605268 | ||
|
|
f16c3857e5 | ||
|
|
5298511bb9 | ||
|
|
4400ab1da9 | ||
|
|
6a9977b954 | ||
|
|
c0d5f55baa | ||
|
|
68f6ae8f3a | ||
|
|
53c691898f | ||
|
|
365af9c54b | ||
|
|
55b1a66ec0 | ||
|
|
522e5a49a2 | ||
|
|
e99febb52d | ||
|
|
2036c8abaf | ||
|
|
021a5b833a | ||
|
|
f1042de9c7 | ||
|
|
efd3d40aa8 | ||
|
|
9c7062020e | ||
|
|
9da80c121b | ||
|
|
5380724323 | ||
|
|
77651701aa | ||
|
|
bb834f2e0a | ||
|
|
535e627048 | ||
|
|
8a74950708 | ||
|
|
3502bdf6c7 |
21
.vscode/tasks.json
vendored
@@ -9,24 +9,33 @@
|
|||||||
// A task runner that calls a custom npm script that compiles the extension.
|
// A task runner that calls a custom npm script that compiles the extension.
|
||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"showOutput": "always",
|
"showOutput": "silent",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"taskName": "compile",
|
"taskName": "compile",
|
||||||
"command": "npm run compile",
|
"command": "npm run compile --silent",
|
||||||
"isBuildCommand": true,
|
"isBuildCommand": true,
|
||||||
"isShellCommand": true,
|
"isShellCommand": true,
|
||||||
"problemMatcher": [ "$tsc", "$tslint5" ]
|
"problemMatcher": [
|
||||||
|
"$tsc",
|
||||||
|
{
|
||||||
|
"base": "$tslint5",
|
||||||
|
"fileLocation": "relative"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"taskName": "lint",
|
"taskName": "lint",
|
||||||
"command": "npm run lint",
|
"command": "npm run lint --silent",
|
||||||
"isShellCommand": true,
|
"isShellCommand": true,
|
||||||
"problemMatcher": "$tslint5"
|
"problemMatcher": {
|
||||||
|
"base": "$tslint5",
|
||||||
|
"fileLocation": "relative"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"taskName": "watch",
|
"taskName": "watch",
|
||||||
"command": "npm run watch",
|
"command": "npm run watch --silent",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"isShellCommand": true,
|
"isShellCommand": true,
|
||||||
"problemMatcher": "$tsc-watch"
|
"problemMatcher": "$tsc-watch"
|
||||||
|
|||||||
102
CHANGELOG.md
@@ -4,20 +4,111 @@ 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] - 2017-05-23
|
## [4.0.2-beta] - 2017-06-10
|
||||||
|
### Added
|
||||||
|
- Improves performance
|
||||||
|
- Optimized git output parsing to increase speed and reduce memory usage
|
||||||
|
- Defers diff chunk parsing until it is actually required
|
||||||
|
|
||||||
|
### 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 Zack Schuster (@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
|
||||||
@@ -26,6 +117,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
|
||||||
@@ -59,7 +153,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
|
||||||
|
|||||||
242
README.md
@@ -5,58 +5,76 @@
|
|||||||
|
|
||||||
# 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
|

|
||||||

|
|
||||||
|
|
||||||
#### Featuring selected line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more
|
|
||||||

|
|
||||||
|
|
||||||
## 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
|

|
||||||
- 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)
|

|
||||||
- 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
|

|
||||||
- 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)
|
||||||
|
|
||||||
|

|
||||||
|
- 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 Code Lens
|
||||||
|
|
||||||
|
- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default)
|
||||||
|
|
||||||
|

|
||||||
- **Recent Change** — author and date of the most recent commit for the file or code block
|
- **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 +86,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 +106,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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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 +117,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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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 +125,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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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 +136,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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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 +157,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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- [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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 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 +182,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,48 +192,132 @@ 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
- If the `Copy to * clipboard` commands don't work on Linux -- `xclip` needs to be installed. You can install it via `sudo apt-get install xclip`
|
- If the `Copy to * clipboard` commands don't work on Linux -- `xclip` needs to be installed. You can install it via `sudo apt-get install xclip`
|
||||||
- Visible whitespace causes issues ([vscode issue #11485](https://github.com/Microsoft/vscode/issues/11485)) with the `expanded` and `compact` blame annotation styles when using a non-monospace font -- set `"gitlens.advanced.toggleWhitespace.enabled": true` if you are using a non-monospace font
|
- Visible whitespace causes issues ([vscode issue #11485](https://github.com/Microsoft/vscode/issues/11485)) with the `expanded` and `compact` blame annotation styles when using a non-monospace font -- set `"gitlens.advanced.toggleWhitespace.enabled": true` if you are using a non-monospace font
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
A big thanks to the people that have contributed to this project:
|
||||||
|
|
||||||
|
- Aurelio Ogliari ([@nobitagit](https://github.com/nobitagit)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=nobitagit)
|
||||||
|
- Zack Schuster ([@zackschuster](https://github.com/zackschuster)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=zackschuster)
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
BIN
images/gitlens-preview-full.gif
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
images/gitlens-preview.gif
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
images/screenshot-code-lens.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
images/screenshot-file-blame-annotations.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
images/screenshot-line-blame-annotation.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
images/screenshot-line-blame-annotations.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
images/screenshot-status-bar.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
1940
package-lock.json
generated
Normal file
871
package.json
14
src/@types/applicationinsights/index.d.ts
vendored
@@ -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" {
|
||||||
|
|||||||
@@ -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,8 +55,8 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
293
src/annotations/annotationController.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
'use strict';
|
||||||
|
import { Functions, Objects } from '../system';
|
||||||
|
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
|
||||||
|
import { AnnotationProviderBase } from './annotationProvider';
|
||||||
|
import { TextDocumentComparer, TextEditorComparer } from '../comparers';
|
||||||
|
import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration';
|
||||||
|
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
|
||||||
|
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
|
||||||
|
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { WhitespaceController } from './whitespaceController';
|
||||||
|
|
||||||
|
export const Decorations = {
|
||||||
|
annotation: window.createTextEditorDecorationType({
|
||||||
|
isWholeLine: true,
|
||||||
|
textDecoration: 'none'
|
||||||
|
} as DecorationRenderOptions),
|
||||||
|
highlight: 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.annotation && Decorations.annotation.dispose();
|
||||||
|
Decorations.highlight && Decorations.highlight.dispose();
|
||||||
|
|
||||||
|
this._annotationsDisposable && this._annotationsDisposable.dispose();
|
||||||
|
this._whitespaceController && this._whitespaceController.dispose();
|
||||||
|
this._disposable && this._disposable.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onConfigurationChanged() {
|
||||||
|
let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get<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 cfgHighlight = cfg.blame.file.lineHighlight;
|
||||||
|
const cfgTheme = cfg.theme.lineHighlight;
|
||||||
|
|
||||||
|
if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) ||
|
||||||
|
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
Decorations.highlight && Decorations.highlight.dispose();
|
||||||
|
|
||||||
|
if (cfgHighlight.enabled) {
|
||||||
|
Decorations.highlight = window.createTextEditorDecorationType({
|
||||||
|
gutterIconSize: 'contain',
|
||||||
|
isWholeLine: true,
|
||||||
|
overviewRulerLane: OverviewRulerLane.Right,
|
||||||
|
dark: {
|
||||||
|
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
|
||||||
|
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
|
||||||
|
: undefined,
|
||||||
|
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
|
||||||
|
? this.context.asAbsolutePath('images/blame-dark.svg')
|
||||||
|
: undefined,
|
||||||
|
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
|
||||||
|
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
|
||||||
|
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
|
||||||
|
: undefined,
|
||||||
|
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
|
||||||
|
? this.context.asAbsolutePath('images/blame-light.svg')
|
||||||
|
: undefined,
|
||||||
|
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
|
||||||
|
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Decorations.highlight = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
|
||||||
|
!Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) ||
|
||||||
|
!Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = cfg;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
// Since the configuration has changed -- reset any visible annotations
|
||||||
|
for (const provider of this._annotationProviders.values()) {
|
||||||
|
if (provider === undefined) continue;
|
||||||
|
|
||||||
|
provider.reset(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.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
|
||||||
|
break;
|
||||||
|
case FileAnnotationType.Hover:
|
||||||
|
provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (provider === undefined || !(await provider.validate())) return false;
|
||||||
|
|
||||||
|
if (currentProvider) {
|
||||||
|
await this.clear(currentProvider.editor.viewColumn || -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._annotationsDisposable && this._annotationProviders.size === 0) {
|
||||||
|
Logger.log(`Add listener registrations for annotations`);
|
||||||
|
|
||||||
|
const subscriptions: Disposable[] = [];
|
||||||
|
|
||||||
|
subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
|
||||||
|
subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
|
||||||
|
subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
|
||||||
|
subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
|
||||||
|
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
|
||||||
|
|
||||||
|
this._annotationsDisposable = Disposable.from(...subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._annotationProviders.set(editor.viewColumn || -1, provider);
|
||||||
|
if (await provider.provideAnnotation(shaOrLine)) {
|
||||||
|
this._onDidToggleAnnotations.fire();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<boolean> {
|
||||||
|
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
|
||||||
|
|
||||||
|
const provider = this._annotationProviders.get(editor.viewColumn || -1);
|
||||||
|
if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine);
|
||||||
|
|
||||||
|
await this.clear(provider.editor.viewColumn || -1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
|
||||||
|
if (e.blameable || !e.editor) return;
|
||||||
|
|
||||||
|
for (const [key, p] of this._annotationProviders) {
|
||||||
|
if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;
|
||||||
|
|
||||||
|
Logger.log('BlameabilityChanged:', `Clear annotations for column ${key}`);
|
||||||
|
this.clear(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
|
||||||
|
for (const [key, p] of this._annotationProviders) {
|
||||||
|
if (!TextDocumentComparer.equals(p.document, e.document)) continue;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/annotations/annotationProvider.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
'use strict';
|
||||||
|
import { Functions } from '../system';
|
||||||
|
import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
|
||||||
|
import { TextDocumentComparer } from '../comparers';
|
||||||
|
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
|
||||||
|
import { WhitespaceController } from './whitespaceController';
|
||||||
|
|
||||||
|
export abstract class AnnotationProviderBase extends Disposable {
|
||||||
|
|
||||||
|
public annotationType: FileAnnotationType;
|
||||||
|
public document: TextDocument;
|
||||||
|
|
||||||
|
protected _config: IConfig;
|
||||||
|
protected _disposable: Disposable;
|
||||||
|
|
||||||
|
constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) {
|
||||||
|
super(() => this.dispose());
|
||||||
|
|
||||||
|
this.document = this.editor.document;
|
||||||
|
|
||||||
|
this._config = workspace.getConfiguration().get<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 {
|
||||||
|
this.editor.setDecorations(this.decoration, []);
|
||||||
|
this.highlightDecoration && this.editor.setDecorations(this.highlightDecoration, []);
|
||||||
|
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
|
||||||
|
if (this.highlightDecoration !== undefined) {
|
||||||
|
await Functions.wait(1);
|
||||||
|
|
||||||
|
if (this.highlightDecoration === undefined) return;
|
||||||
|
|
||||||
|
this.editor.setDecorations(this.highlightDecoration, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
|
||||||
|
this.whitespaceController && await this.whitespaceController.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset(whitespaceController: WhitespaceController | undefined) {
|
||||||
|
await this.clear();
|
||||||
|
|
||||||
|
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
||||||
|
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>;
|
||||||
|
}
|
||||||
191
src/annotations/annotations.ts
Normal 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 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/annotations/blameAnnotationProvider.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
'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, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/annotations/diffAnnotationProvider.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode';
|
||||||
|
import { AnnotationProviderBase } from './annotationProvider';
|
||||||
|
import { GitService, GitUri } from '../gitService';
|
||||||
|
import { WhitespaceController } from './whitespaceController';
|
||||||
|
|
||||||
|
export class DiffAnnotationProvider extends AnnotationProviderBase {
|
||||||
|
|
||||||
|
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, private git: GitService, private uri: GitUri) {
|
||||||
|
super(context, editor, decoration, highlightDecoration, whitespaceController);
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
|
||||||
|
// let sha1: string | undefined = undefined;
|
||||||
|
// let sha2: string | undefined = undefined;
|
||||||
|
// if (shaOrLine === undefined) {
|
||||||
|
// const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true });
|
||||||
|
// if (commit === undefined) return false;
|
||||||
|
|
||||||
|
// sha1 = commit.previousSha;
|
||||||
|
// }
|
||||||
|
// else if (typeof shaOrLine === 'string') {
|
||||||
|
// sha1 = shaOrLine;
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// const blame = await this.git.getBlameForLine(this.uri, shaOrLine);
|
||||||
|
// if (blame === undefined) return false;
|
||||||
|
|
||||||
|
// sha1 = blame.commit.previousSha;
|
||||||
|
// sha2 = blame.commit.sha;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (sha1 === undefined) return false;
|
||||||
|
|
||||||
|
const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true });
|
||||||
|
if (commit === undefined) return false;
|
||||||
|
|
||||||
|
const diff = await this.git.getDiffForFile(this.uri, commit.previousSha);
|
||||||
|
if (diff === undefined) return false;
|
||||||
|
|
||||||
|
const decorators: DecorationOptions[] = [];
|
||||||
|
|
||||||
|
for (const chunk of diff.chunks) {
|
||||||
|
let count = chunk.currentPosition.start - 2;
|
||||||
|
for (const change of chunk.current) {
|
||||||
|
if (change === undefined) continue;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (change.state === 'unchanged') continue;
|
||||||
|
|
||||||
|
decorators.push({
|
||||||
|
range: new Range(new Position(count, 0), new Position(count, 0))
|
||||||
|
} as DecorationOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.setDecorations(this.decoration, decorators);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async selection(shaOrLine?: string | number): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/annotations/gutterBlameAnnotationProvider.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
import { Strings } from '../system';
|
||||||
|
import { DecorationOptions, Range } from 'vscode';
|
||||||
|
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
|
||||||
|
import { Annotations, endOfLineIndex } from './annotations';
|
||||||
|
import { FileAnnotationType } from '../configuration';
|
||||||
|
import { ICommitFormatOptions } from '../gitService';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
|
||||||
|
|
||||||
|
async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/annotations/hoverBlameAnnotationProvider.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
'use strict';
|
||||||
|
import { DecorationOptions, Range } from 'vscode';
|
||||||
|
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
|
||||||
|
import { Annotations, endOfLineIndex } from './annotations';
|
||||||
|
import { FileAnnotationType } from '../configuration';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
|
||||||
|
|
||||||
|
async provideAnnotation(shaOrLine?: string | number): Promise<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
import { Functions, Objects } from './system';
|
|
||||||
import { DecorationOptions, DecorationInstanceRenderOptions, 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: boolean = 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;
|
|
||||||
|
|
||||||
let 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)}\` __Uncommitted change__\n\n\---\n\`\`\`\n${original}\n\`\`\``;
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// hoverMessage = `\`${'0'.repeat(8)}\` __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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, GitService, GitUri, GitContextTracker } 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;
|
|
||||||
|
|
||||||
let 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 | Array<string> {
|
|
||||||
const message = `> \`${commit.message.replace(/\n/g, '\`\n>\n> \`')}\``;
|
|
||||||
if (commit.isUncommitted) {
|
|
||||||
return `\`${'0'.repeat(8)}\` __Uncommitted change__`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `\`${commit.shortSha}\` __${commit.author}__, ${moment(commit.date).fromNow()} _(${moment(commit.date).format(config.annotation.dateFormat || 'MMMM Do, YYYY h:MMa')})_ \n\n${message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getAuthorAndDate(config: IBlameConfig, commit: GitCommit, format: string, force: boolean = false) {
|
|
||||||
if (!force && !config.annotation.author && (!config.annotation.date || config.annotation.date === 'off')) return '';
|
|
||||||
|
|
||||||
if (!config.annotation.author) {
|
|
||||||
return this.getDate(config, commit, format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.annotation.date || config.annotation.date === 'off') {
|
|
||||||
return this.getAuthor(config, commit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${this.getAuthor(config, commit)}, ${this.getDate(config, commit, format)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getAuthor(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) {
|
|
||||||
if (!force && !config.annotation.author) return '';
|
|
||||||
|
|
||||||
const author = commit.isUncommitted ? 'Uncommitted' : commit.author;
|
|
||||||
if (!truncateTo) return author;
|
|
||||||
|
|
||||||
if (author.length > truncateTo) {
|
|
||||||
return `${author.substring(0, truncateTo - 1)}\u2026`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force) return author; // Don't pad when just asking for the value
|
|
||||||
return author + '\u00a0'.repeat(truncateTo - author.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDate(config: IBlameConfig, commit: GitCommit, format: string, truncate: boolean = false, force: boolean = false) {
|
|
||||||
if (!force && (!config.annotation.date || config.annotation.date === 'off')) return '';
|
|
||||||
|
|
||||||
const date = config.annotation.date === 'relative'
|
|
||||||
? moment(commit.date).fromNow()
|
|
||||||
: moment(commit.date).format(format);
|
|
||||||
if (!truncate) return date;
|
|
||||||
|
|
||||||
const truncateTo = config.annotation.date === 'relative' ? defaultRelativeDateLength : defaultAbsoluteDateLength;
|
|
||||||
if (date.length > truncateTo) {
|
|
||||||
return `${date.substring(0, truncateTo - 1)}\u2026`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force) return date; // Don't pad when just asking for the value
|
|
||||||
return date + '\u00a0'.repeat(truncateTo - date.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getMessage(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) {
|
|
||||||
if (!force && !config.annotation.message) return '';
|
|
||||||
|
|
||||||
let message = commit.isUncommitted ? 'Uncommitted change' : commit.message;
|
|
||||||
if (truncateTo && message.length > truncateTo) {
|
|
||||||
return `${message.substring(0, truncateTo - 1)}\u2026`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,6 @@ 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/toggleLineBlame';
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,42 @@ 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.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 +57,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 +74,9 @@ 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,
|
||||||
|
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 +85,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,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ 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 { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks';
|
import { Messages } from '../messages';
|
||||||
|
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
|
||||||
|
|
||||||
export interface DiffDirectoryCommandCommandArgs {
|
export interface DiffDirectoryCommandCommandArgs {
|
||||||
shaOrBranch1?: string;
|
shaOrBranch1?: string;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ 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 { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks';
|
import { Messages } from '../messages';
|
||||||
|
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export interface DiffWithBranchCommandArgs {
|
export interface DiffWithBranchCommandArgs {
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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})`);
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ export const keys: Keys[] = [
|
|||||||
'.'
|
'.'
|
||||||
];
|
];
|
||||||
|
|
||||||
export declare type KeyMapping = { [id: string]: (QuickPickItem | (() => Promise<QuickPickItem>) | undefined) };
|
export declare interface KeyMapping {
|
||||||
let mappings: KeyMapping[] = [];
|
[id: string]: (QuickPickItem | (() => Promise<QuickPickItem>) | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappings: KeyMapping[] = [];
|
||||||
|
|
||||||
let _instance: Keyboard;
|
let _instance: Keyboard;
|
||||||
|
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand {
|
|||||||
return command.execute();
|
return command.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
let placeHolder: string = '';
|
let placeHolder = '';
|
||||||
switch (args.resource.type) {
|
switch (args.resource.type) {
|
||||||
case 'branch':
|
case 'branch':
|
||||||
// Check to see if the remote is in the branch
|
// Check to see if the remote is in the branch
|
||||||
|
|||||||
18
src/commands/resetSuppressedWarnings.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
35
src/commands/showFileBlame.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
|
||||||
|
import { AnnotationController } from '../annotations/annotationController';
|
||||||
|
import { Commands, EditorCommand } from './common';
|
||||||
|
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
|
export interface ShowFileBlameCommandArgs {
|
||||||
|
sha?: string;
|
||||||
|
type?: FileAnnotationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShowFileBlameCommand extends EditorCommand {
|
||||||
|
|
||||||
|
constructor(private annotationController: AnnotationController) {
|
||||||
|
super(Commands.ShowFileBlame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise<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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/commands/showLineBlame.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
|
||||||
|
import { CurrentLineController } from '../currentLineController';
|
||||||
|
import { Commands, EditorCommand } from './common';
|
||||||
|
import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
|
export interface ShowLineBlameCommandArgs {
|
||||||
|
type?: LineAnnotationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShowLineBlameCommand extends EditorCommand {
|
||||||
|
|
||||||
|
constructor(private currentLineController: CurrentLineController) {
|
||||||
|
super(Commands.ShowLineBlame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise<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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
src/commands/toggleFileBlame.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
|
||||||
|
import { AnnotationController } from '../annotations/annotationController';
|
||||||
|
import { Commands, EditorCommand } from './common';
|
||||||
|
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
|
export interface ToggleFileBlameCommandArgs {
|
||||||
|
sha?: string;
|
||||||
|
type?: FileAnnotationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToggleFileBlameCommand extends EditorCommand {
|
||||||
|
|
||||||
|
constructor(private annotationController: AnnotationController) {
|
||||||
|
super(Commands.ToggleFileBlame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise<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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/commands/toggleLineBlame.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
|
||||||
|
import { CurrentLineController } from '../currentLineController';
|
||||||
|
import { Commands, EditorCommand } from './common';
|
||||||
|
import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
|
export interface ToggleLineBlameCommandArgs {
|
||||||
|
type?: LineAnnotationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToggleLineBlameCommand extends EditorCommand {
|
||||||
|
|
||||||
|
constructor(private currentLineController: CurrentLineController) {
|
||||||
|
super(Commands.ToggleLineBlame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise<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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,31 +2,25 @@
|
|||||||
import { Commands } from './commands';
|
import { Commands } from './commands';
|
||||||
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,
|
export type BlameLineHighlightLocations = 'gutter' | 'line' | 'overviewRuler';
|
||||||
Expanded: 'expanded' as BlameAnnotationStyle,
|
export const BlameLineHighlightLocations = {
|
||||||
Trailing: 'trailing' as BlameAnnotationStyle
|
Gutter: 'gutter' as BlameLineHighlightLocations,
|
||||||
|
Line: 'line' as BlameLineHighlightLocations,
|
||||||
|
OverviewRuler: 'overviewRuler' as BlameLineHighlightLocations
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +30,42 @@ 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 FileAnnotationType = 'gutter' | 'hover';
|
||||||
export const CodeLensVisibility = {
|
export const FileAnnotationType = {
|
||||||
Auto: 'auto' as CodeLensVisibility,
|
Gutter: 'gutter' as FileAnnotationType,
|
||||||
OnDemand: 'ondemand' as CodeLensVisibility,
|
Hover: 'hover' as FileAnnotationType
|
||||||
Off: 'off' as CodeLensVisibility
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ICodeLensConfig {
|
export type LineAnnotationType = 'trailing' | 'hover';
|
||||||
enabled: boolean;
|
export const LineAnnotationType = {
|
||||||
command: CodeLensCommand;
|
Trailing: 'trailing' as LineAnnotationType,
|
||||||
}
|
Hover: 'hover' as LineAnnotationType
|
||||||
|
};
|
||||||
|
|
||||||
export interface ICodeLensLanguageLocation {
|
export type StatusBarCommand = 'gitlens.toggleFileBlame' |
|
||||||
language: string | undefined;
|
'gitlens.showBlameHistory' |
|
||||||
location: CodeLensLocation;
|
'gitlens.showFileHistory' |
|
||||||
customSymbols?: string[];
|
'gitlens.toggleCodeLens' |
|
||||||
}
|
'gitlens.diffWithPrevious' |
|
||||||
|
'gitlens.diffWithWorking' |
|
||||||
export interface ICodeLensesConfig {
|
'gitlens.showQuickCommitDetails' |
|
||||||
debug: boolean;
|
'gitlens.showQuickCommitFileDetails' |
|
||||||
visibility: CodeLensVisibility;
|
'gitlens.showQuickFileHistory' |
|
||||||
location: CodeLensLocation;
|
'gitlens.showQuickRepoHistory';
|
||||||
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 +73,251 @@ 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
line: {
|
||||||
|
hover: {
|
||||||
|
details: boolean;
|
||||||
|
changes: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
trailing: {
|
||||||
|
format: string;
|
||||||
|
dateFormat: string;
|
||||||
|
hover: {
|
||||||
|
changes: boolean;
|
||||||
|
details: boolean;
|
||||||
|
wholeLine: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
blame: {
|
||||||
|
file: {
|
||||||
|
annotationType: FileAnnotationType;
|
||||||
|
lineHighlight: {
|
||||||
|
enabled: boolean;
|
||||||
|
locations: BlameLineHighlightLocations[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
line: {
|
||||||
|
enabled: boolean;
|
||||||
|
annotationType: LineAnnotationType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
codeLens: {
|
||||||
|
enabled: boolean;
|
||||||
|
recentChange: {
|
||||||
|
enabled: boolean;
|
||||||
|
command: CodeLensCommand;
|
||||||
|
};
|
||||||
|
authors: {
|
||||||
|
enabled: boolean;
|
||||||
|
command: CodeLensCommand;
|
||||||
|
};
|
||||||
|
locations: CodeLensLocations[];
|
||||||
|
customLocationSymbols: string[];
|
||||||
|
perLanguageLocations: ICodeLensLanguageLocation[];
|
||||||
|
debug: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
statusBar: {
|
||||||
|
enabled: boolean;
|
||||||
|
alignment: 'left' | 'right';
|
||||||
|
command: StatusBarCommand;
|
||||||
|
format: string;
|
||||||
|
dateFormat: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
441
src/currentLineController.ts
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
'use strict';
|
||||||
|
import { Functions, Objects } from './system';
|
||||||
|
import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
|
||||||
|
import { AnnotationController } from './annotations/annotationController';
|
||||||
|
import { Annotations, endOfLineIndex } from './annotations/annotations';
|
||||||
|
import { Commands } from './commands';
|
||||||
|
import { TextEditorComparer } from './comparers';
|
||||||
|
import { FileAnnotationType, IConfig, LineAnnotationType, StatusBarCommand } from './configuration';
|
||||||
|
import { DocumentSchemes, ExtensionKey } from './constants';
|
||||||
|
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri } from './gitService';
|
||||||
|
|
||||||
|
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
|
||||||
|
after: {
|
||||||
|
margin: '0 0 0 3em',
|
||||||
|
textDecoration: 'none'
|
||||||
|
}
|
||||||
|
} as DecorationRenderOptions);
|
||||||
|
|
||||||
|
export class CurrentLineController extends Disposable {
|
||||||
|
|
||||||
|
private _activeEditorLineDisposable: Disposable | undefined;
|
||||||
|
private _blameable: boolean;
|
||||||
|
private _config: IConfig;
|
||||||
|
private _currentLine: number = -1;
|
||||||
|
private _disposable: Disposable;
|
||||||
|
private _editor: TextEditor | undefined;
|
||||||
|
private _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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/extension.ts
@@ -1,33 +1,36 @@
|
|||||||
'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, 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';
|
||||||
import { ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowCommitSearchCommand } from './commands';
|
import { ShowCommitSearchCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand } from './commands';
|
||||||
import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './commands';
|
import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './commands';
|
||||||
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 { BlameLineHighlightLocations, CodeLensLocations, IConfig, LineAnnotationType } from './configuration';
|
||||||
import { ApplicationInsightsKey, BuiltInCommands, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants';
|
import { ApplicationInsightsKey, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants';
|
||||||
|
import { CurrentLineController } 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,11 @@ 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 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 +125,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', [BlameLineHighlightLocations.Gutter, BlameLineHighlightLocations.OverviewRuler], true);
|
||||||
|
break;
|
||||||
|
case 'line':
|
||||||
|
await cfg.update('blame.file.lineHighlight.locations', [BlameLineHighlightLocations.Line, BlameLineHighlightLocations.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
160
src/git/formatters/commit.ts
Normal 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}\` __${commit.author}__, ${moment(commit.date).fromNow()} _(${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\` \u2014 _uncommitted_\n${codeDiff}`
|
||||||
|
: `\`Changes\` \u2014 \`${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()}
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { spawnPromise } from 'spawn-rx';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
|
import * as iconv from 'iconv-lite';
|
||||||
|
|
||||||
export { IGit };
|
export { IGit };
|
||||||
export * from './models/models';
|
export * from './models/models';
|
||||||
@@ -21,25 +22,43 @@ let git: IGit;
|
|||||||
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--date=iso8601`, `--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?`];
|
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--date=iso8601`, `--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?`];
|
||||||
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %ai%nreflog-selector %gd%nsummary %B%nfilename ?`];
|
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %ai%nreflog-selector %gd%nsummary %B%nfilename ?`];
|
||||||
|
|
||||||
async function gitCommand(cwd: string, ...args: any[]) {
|
let defaultEncoding = 'utf8';
|
||||||
|
export function setDefaultEncoding(encoding: string) {
|
||||||
|
defaultEncoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
|
||||||
|
}
|
||||||
|
|
||||||
|
const GitWarnings = [
|
||||||
|
/Not a git repository/,
|
||||||
|
/is outside repository/,
|
||||||
|
/no such path/,
|
||||||
|
/does not have any commits/
|
||||||
|
];
|
||||||
|
|
||||||
|
async function gitCommand(options: { cwd: string, encoding?: string }, ...args: any[]) {
|
||||||
try {
|
try {
|
||||||
// Fixes https://github.com/eamodio/vscode-gitlens/issues/73
|
// Fixes https://github.com/eamodio/vscode-gitlens/issues/73
|
||||||
// See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x
|
// See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x
|
||||||
args.splice(0, 0, '-c', 'core.quotepath=false');
|
args.splice(0, 0, '-c', 'core.quotepath=false');
|
||||||
|
|
||||||
const s = await spawnPromise(git.path, args, { cwd: cwd });
|
const opts = { encoding: 'utf8', ...options };
|
||||||
Logger.log('git', ...args, ` cwd='${cwd}'`);
|
const s = await spawnPromise(git.path, args, { cwd: options.cwd, encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' });
|
||||||
return s;
|
Logger.log('git', ...args, ` cwd='${options.cwd}'`);
|
||||||
|
if (opts.encoding === 'utf8' || opts.encoding === 'binary') return s;
|
||||||
|
|
||||||
|
return iconv.decode(Buffer.from(s, 'binary'), opts.encoding);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
const msg = ex && ex.toString();
|
const msg = ex && ex.toString();
|
||||||
if (msg && (msg.includes('Not a git repository') || msg.includes('is outside repository') || msg.includes('no such path') || msg.includes('does not have any commits'))) {
|
if (msg) {
|
||||||
Logger.warn('git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
|
for (const warning of GitWarnings) {
|
||||||
return '';
|
if (warning.test(msg)) {
|
||||||
}
|
Logger.warn('git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
|
||||||
else {
|
return '';
|
||||||
Logger.error(ex, 'git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,14 +81,14 @@ export class Git {
|
|||||||
static async getRepoPath(cwd: string | undefined) {
|
static async getRepoPath(cwd: string | undefined) {
|
||||||
if (cwd === undefined) return '';
|
if (cwd === undefined) return '';
|
||||||
|
|
||||||
const data = await gitCommand(cwd, 'rev-parse', '--show-toplevel');
|
const data = await gitCommand({ cwd }, 'rev-parse', '--show-toplevel');
|
||||||
if (!data) return '';
|
if (!data) return '';
|
||||||
|
|
||||||
return data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/');
|
return data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) {
|
static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) {
|
||||||
const data = await Git.show(repoPath, fileName, branchOrSha);
|
const data = await Git.show(repoPath, fileName, branchOrSha, 'binary');
|
||||||
|
|
||||||
const suffix = Git.isSha(branchOrSha) ? branchOrSha.substring(0, 8) : branchOrSha;
|
const suffix = Git.isSha(branchOrSha) ? branchOrSha.substring(0, 8) : branchOrSha;
|
||||||
const ext = path.extname(fileName);
|
const ext = path.extname(fileName);
|
||||||
@@ -82,7 +101,7 @@ export class Git {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${branchOrSha}); destination=${destination}`);
|
Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${branchOrSha}); destination=${destination}`);
|
||||||
fs.appendFile(destination, data, err => {
|
fs.appendFile(destination, data, { encoding: 'binary' }, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
@@ -144,7 +163,7 @@ export class Git {
|
|||||||
params.push(sha);
|
params.push(sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(root, ...params, `--`, file);
|
return gitCommand({ cwd: root }, ...params, `--`, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
static branch(repoPath: string, all: boolean) {
|
static branch(repoPath: string, all: boolean) {
|
||||||
@@ -153,19 +172,19 @@ export class Git {
|
|||||||
params.push(`-a`);
|
params.push(`-a`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(repoPath, ...params);
|
return gitCommand({ cwd: repoPath }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async config_get(key: string, repoPath?: string) {
|
static async config_get(key: string, repoPath?: string) {
|
||||||
try {
|
try {
|
||||||
return await gitCommand(repoPath || '', `config`, `--get`, key);
|
return await gitCommand({ cwd: repoPath || '' }, `config`, `--get`, key);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string) {
|
static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string, encoding?: string) {
|
||||||
const params = [`diff`, `--diff-filter=M`, `-M`];
|
const params = [`diff`, `--diff-filter=M`, `-M`];
|
||||||
if (sha1) {
|
if (sha1) {
|
||||||
params.push(sha1);
|
params.push(sha1);
|
||||||
@@ -174,7 +193,7 @@ export class Git {
|
|||||||
params.push(sha2);
|
params.push(sha2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(repoPath, ...params, '--', fileName);
|
return gitCommand({ cwd: repoPath, encoding: encoding || defaultEncoding }, ...params, '--', fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static diff_nameStatus(repoPath: string, sha1?: string, sha2?: string) {
|
static diff_nameStatus(repoPath: string, sha1?: string, sha2?: string) {
|
||||||
@@ -186,7 +205,7 @@ export class Git {
|
|||||||
params.push(sha2);
|
params.push(sha2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(repoPath, ...params);
|
return gitCommand({ cwd: repoPath }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static difftool_dirDiff(repoPath: string, sha1: string, sha2?: string) {
|
static difftool_dirDiff(repoPath: string, sha1: string, sha2?: string) {
|
||||||
@@ -195,7 +214,7 @@ export class Git {
|
|||||||
params.push(sha2);
|
params.push(sha2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(repoPath, ...params);
|
return gitCommand({ cwd: repoPath }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) {
|
static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) {
|
||||||
@@ -213,7 +232,7 @@ export class Git {
|
|||||||
params.push(sha);
|
params.push(sha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return gitCommand(repoPath, ...params);
|
return gitCommand({ cwd: repoPath }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static log_file(repoPath: string, fileName: string, sha?: string, maxCount?: number, reverse: boolean = false, startLine?: number, endLine?: number) {
|
static log_file(repoPath: string, fileName: string, sha?: string, maxCount?: number, reverse: boolean = false, startLine?: number, endLine?: number) {
|
||||||
@@ -250,7 +269,7 @@ export class Git {
|
|||||||
params.push(`--`);
|
params.push(`--`);
|
||||||
params.push(file);
|
params.push(file);
|
||||||
|
|
||||||
return gitCommand(root, ...params);
|
return gitCommand({ cwd: root }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static log_search(repoPath: string, search: string[] = [], maxCount?: number) {
|
static log_search(repoPath: string, search: string[] = [], maxCount?: number) {
|
||||||
@@ -259,12 +278,12 @@ export class Git {
|
|||||||
params.push(`-n${maxCount}`);
|
params.push(`-n${maxCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gitCommand(repoPath, ...params, ...search);
|
return gitCommand({ cwd: repoPath }, ...params, ...search);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async ls_files(repoPath: string, fileName: string): Promise<string> {
|
static async ls_files(repoPath: string, fileName: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await gitCommand(repoPath, 'ls-files', fileName);
|
return await gitCommand({ cwd: repoPath }, 'ls-files', fileName);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
return '';
|
return '';
|
||||||
@@ -272,33 +291,33 @@ export class Git {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static remote(repoPath: string): Promise<string> {
|
static remote(repoPath: string): Promise<string> {
|
||||||
return gitCommand(repoPath, 'remote', '-v');
|
return gitCommand({ cwd: repoPath }, 'remote', '-v');
|
||||||
}
|
}
|
||||||
|
|
||||||
static remote_url(repoPath: string, remote: string): Promise<string> {
|
static remote_url(repoPath: string, remote: string): Promise<string> {
|
||||||
return gitCommand(repoPath, 'remote', 'get-url', remote);
|
return gitCommand({ cwd: repoPath }, 'remote', 'get-url', remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
static show(repoPath: string | undefined, fileName: string, branchOrSha: string) {
|
static show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) {
|
||||||
const [file, root] = Git.splitPath(fileName, repoPath);
|
const [file, root] = Git.splitPath(fileName, repoPath);
|
||||||
branchOrSha = branchOrSha.replace('^', '');
|
branchOrSha = branchOrSha.replace('^', '');
|
||||||
|
|
||||||
if (Git.isUncommitted(branchOrSha)) return Promise.reject(new Error(`sha=${branchOrSha} is uncommitted`));
|
if (Git.isUncommitted(branchOrSha)) return Promise.reject(new Error(`sha=${branchOrSha} is uncommitted`));
|
||||||
return gitCommand(root, 'show', `${branchOrSha}:./${file}`);
|
return gitCommand({ cwd: root, encoding: encoding || defaultEncoding }, 'show', `${branchOrSha}:./${file}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stash_apply(repoPath: string, stashName: string, deleteAfter: boolean) {
|
static stash_apply(repoPath: string, stashName: string, deleteAfter: boolean) {
|
||||||
if (!stashName) return undefined;
|
if (!stashName) return undefined;
|
||||||
return gitCommand(repoPath, 'stash', deleteAfter ? 'pop' : 'apply', stashName);
|
return gitCommand({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stash_delete(repoPath: string, stashName: string) {
|
static stash_delete(repoPath: string, stashName: string) {
|
||||||
if (!stashName) return undefined;
|
if (!stashName) return undefined;
|
||||||
return gitCommand(repoPath, 'stash', 'drop', stashName);
|
return gitCommand({ cwd: repoPath }, 'stash', 'drop', stashName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stash_list(repoPath: string) {
|
static stash_list(repoPath: string) {
|
||||||
return gitCommand(repoPath, ...defaultStashParams);
|
return gitCommand({ cwd: repoPath }, ...defaultStashParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stash_save(repoPath: string, message?: string, unstagedOnly: boolean = false) {
|
static stash_save(repoPath: string, message?: string, unstagedOnly: boolean = false) {
|
||||||
@@ -309,18 +328,18 @@ export class Git {
|
|||||||
if (message) {
|
if (message) {
|
||||||
params.push(message);
|
params.push(message);
|
||||||
}
|
}
|
||||||
return gitCommand(repoPath, ...params);
|
return gitCommand({ cwd: repoPath }, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static status(repoPath: string, porcelainVersion: number = 1): Promise<string> {
|
static status(repoPath: string, porcelainVersion: number = 1): Promise<string> {
|
||||||
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
|
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
|
||||||
return gitCommand(repoPath, 'status', porcelain, '--branch');
|
return gitCommand({ cwd: repoPath }, 'status', porcelain, '--branch');
|
||||||
}
|
}
|
||||||
|
|
||||||
static status_file(repoPath: string, fileName: string, porcelainVersion: number = 1): Promise<string> {
|
static status_file(repoPath: string, fileName: string, porcelainVersion: number = 1): Promise<string> {
|
||||||
const [file, root] = Git.splitPath(fileName, repoPath);
|
const [file, root] = Git.splitPath(fileName, repoPath);
|
||||||
|
|
||||||
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
|
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
|
||||||
return gitCommand(root, 'status', porcelain, file);
|
return gitCommand({ cwd: root }, 'status', porcelain, 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);
|
||||||
}
|
}
|
||||||
@@ -72,9 +72,10 @@ export class GitContextTracker extends Disposable {
|
|||||||
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return;
|
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return;
|
||||||
|
|
||||||
// Can't unsubscribe here because undo doesn't trigger any other event
|
// Can't unsubscribe here because undo doesn't trigger any other event
|
||||||
//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);
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ export class GitContextTracker extends Disposable {
|
|||||||
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return;
|
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return;
|
||||||
|
|
||||||
// Don't need to resubscribe as we aren't unsubscribing on document changes anymore
|
// Don't need to resubscribe as we aren't unsubscribing on document changes anymore
|
||||||
//this._subscribeToDocumentChanges();
|
// this._subscribeToDocumentChanges();
|
||||||
this._updateBlameability(!e.isDirty);
|
this._updateBlameability(!e.isDirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
20
src/git/models/blameCommit.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { Uri } from 'vscode';
|
import { Uri } from 'vscode';
|
||||||
import { GitCommit, GitCommitType, IGitCommitLine } from './commit';
|
import { GitCommit, GitCommitType } from './commit';
|
||||||
import { IGitStatusFile, GitStatusFileStatus } from './status';
|
import { GitStatusFileStatus, IGitStatusFile } from './status';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export class GitLogCommit extends GitCommit {
|
export class GitLogCommit extends GitCommit {
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { IGitCommitLine } from './commit';
|
|
||||||
import { GitLogCommit } from './logCommit';
|
import { GitLogCommit } from './logCommit';
|
||||||
import { IGitStatusFile, GitStatusFileStatus } from './status';
|
import { GitStatusFileStatus, IGitStatusFile } from './status';
|
||||||
|
|
||||||
export class GitStashCommit extends GitLogCommit {
|
export class GitStashCommit extends GitLogCommit {
|
||||||
|
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
||||||
let 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: Array<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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
import { Strings } from '../../system';
|
||||||
import { Range } from 'vscode';
|
import { Range } from 'vscode';
|
||||||
import { Git, GitStatusFileStatus, GitLogCommit, GitCommitType, 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 }) {
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { Git, GitStatusFileStatus, GitStatusFile, 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,9 +59,9 @@ export class GitStatusParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let entry: IFileStatusEntry;
|
let entry: FileStatusEntry;
|
||||||
const rawStatus = line.substring(0, 2);
|
const rawStatus = line.substring(0, 2);
|
||||||
let fileName = line.substring(3);
|
const fileName = line.substring(3);
|
||||||
if (rawStatus[0] === 'R') {
|
if (rawStatus[0] === 'R') {
|
||||||
const [file1, file2] = fileName.replace(/\"/g, '').split('->');
|
const [file1, file2] = fileName.replace(/\"/g, '').split('->');
|
||||||
entry = this._parseFileEntry(rawStatus, file2.trim(), file1.trim());
|
entry = this._parseFileEntry(rawStatus, file2.trim(), file1.trim());
|
||||||
@@ -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];
|
||||||
@@ -98,8 +98,8 @@ export class GitStatusParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ export class BitbucketService extends RemoteProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
||||||
let line: string = '';
|
let line = '';
|
||||||
if (range) {
|
if (range) {
|
||||||
if (range.start.line === range.end.line) {
|
if (range.start.line === range.end.line) {
|
||||||
line = `#${fileName}-${range.start.line}`;
|
line = `#${fileName}-${range.start.line}`;
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class GitHubService extends RemoteProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
||||||
let line: string = '';
|
let line = '';
|
||||||
if (range) {
|
if (range) {
|
||||||
if (range.start.line === range.end.line) {
|
if (range.start.line === range.end.line) {
|
||||||
line = `#L${range.start.line}`;
|
line = `#L${range.start.line}`;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class VisualStudioService extends RemoteProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
|
||||||
let line: string = '';
|
let line = '';
|
||||||
if (range) {
|
if (range) {
|
||||||
if (range.start.line === range.end.line) {
|
if (range.start.line === range.end.line) {
|
||||||
line = `&line=${range.start.line}`;
|
line = `&line=${range.start.line}`;
|
||||||
|
|||||||
@@ -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, IConfig, ICodeLensLanguageLocation } 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)));
|
||||||
@@ -115,63 +114,69 @@ 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: boolean = 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,
|
||||||
|
|||||||
@@ -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 } 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,24 +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> {
|
||||||
//date: Date;
|
|
||||||
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,
|
||||||
@@ -89,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());
|
||||||
@@ -130,6 +130,9 @@ export class GitService extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onConfigurationChanged() {
|
private _onConfigurationChanged() {
|
||||||
|
const encoding = workspace.getConfiguration('files').get<string>('encoding', 'utf8');
|
||||||
|
setDefaultEncoding(encoding);
|
||||||
|
|
||||||
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
|
||||||
|
|
||||||
const codeLensChanged = !Objects.areEquivalent(cfg.codeLens, this.config && this.config.codeLens);
|
const codeLensChanged = !Objects.areEquivalent(cfg.codeLens, this.config && this.config.codeLens);
|
||||||
@@ -137,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();
|
||||||
}
|
}
|
||||||
@@ -152,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) {
|
||||||
@@ -164,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));
|
||||||
|
|
||||||
@@ -206,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();
|
||||||
|
|
||||||
@@ -217,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)
|
||||||
@@ -238,7 +258,7 @@ export class GitService extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fileExists(repoPath: string, fileName: string): Promise<boolean> {
|
private async _fileExists(repoPath: string, fileName: string): Promise<boolean> {
|
||||||
return await new Promise<boolean>((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), e => resolve(e)));
|
return await new Promise<boolean>((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise<GitLogCommit | undefined> {
|
async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise<GitLogCommit | undefined> {
|
||||||
@@ -267,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;
|
||||||
}
|
}
|
||||||
@@ -311,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: string = '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})`);
|
||||||
@@ -363,35 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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
|
||||||
@@ -399,29 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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,30 +502,27 @@ 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;
|
||||||
|
|
||||||
const locations: Array<Location> = [];
|
const locations: Location[] = [];
|
||||||
Iterables.forEach(blame.commits.values(), (c, i) => {
|
Iterables.forEach(blame.commits.values(), (c, i) => {
|
||||||
if (c.isUncommitted) return;
|
if (c.isUncommitted) return;
|
||||||
|
|
||||||
@@ -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,14 +565,18 @@ 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> {
|
||||||
let key: string = 'diff';
|
if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) {
|
||||||
|
sha2 = uri.sha;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,29 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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
|
||||||
@@ -626,34 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,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) {
|
||||||
@@ -688,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) {
|
||||||
@@ -721,15 +747,16 @@ 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: string = 'log';
|
let key = 'log';
|
||||||
if (sha !== undefined) {
|
if (sha !== undefined) {
|
||||||
key += `:${sha}`;
|
key += `:${sha}`;
|
||||||
}
|
}
|
||||||
@@ -743,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;
|
||||||
@@ -751,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)`);
|
||||||
@@ -784,27 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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
|
||||||
@@ -812,13 +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, {
|
||||||
//date: new Date(),
|
|
||||||
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;
|
||||||
@@ -829,11 +855,11 @@ 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;
|
||||||
|
|
||||||
const locations: Array<Location> = [];
|
const locations: Location[] = [];
|
||||||
Iterables.forEach(log.commits.values(), (c, i) => {
|
Iterables.forEach(log.commits.values(), (c, i) => {
|
||||||
if (c.isUncommitted) return;
|
if (c.isUncommitted) return;
|
||||||
|
|
||||||
@@ -849,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}')`);
|
||||||
@@ -887,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> {
|
||||||
@@ -906,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) {
|
||||||
@@ -931,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -83,7 +83,7 @@ export class OpenCommitFilesCommandQuickPickItem extends OpenFilesCommandQuickPi
|
|||||||
super(uris, item || {
|
super(uris, item || {
|
||||||
label: `$(file-symlink-file) Open Changed Files`,
|
label: `$(file-symlink-file) Open Changed Files`,
|
||||||
description: `\u00a0 \u2014 \u00a0\u00a0 in \u00a0$(git-commit) ${commit.shortSha}`
|
description: `\u00a0 \u2014 \u00a0\u00a0 in \u00a0$(git-commit) ${commit.shortSha}`
|
||||||
//detail: `Opens all of the changed files in $(git-commit) ${commit.shortSha}`
|
// detail: `Opens all of the changed files in $(git-commit) ${commit.shortSha}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,21 +96,21 @@ export class OpenCommitWorkingTreeFilesCommandQuickPickItem extends OpenFilesCom
|
|||||||
super(uris, item || {
|
super(uris, item || {
|
||||||
label: `$(file-symlink-file) Open Changed Working Files`,
|
label: `$(file-symlink-file) Open Changed Working Files`,
|
||||||
description: ''
|
description: ''
|
||||||
//detail: `Opens all of the changed file in the working tree`
|
// detail: `Opens all of the changed file in the working tree`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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}`
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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, Keys, KeyboardScope, KeyMapping, 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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem {
|
|||||||
constructor(remotes: GitRemote[], resource: RemoteResource, goBackCommand?: CommandQuickPickItem) {
|
constructor(remotes: GitRemote[], resource: RemoteResource, goBackCommand?: CommandQuickPickItem) {
|
||||||
const name = getNameFromRemoteResource(resource);
|
const name = getNameFromRemoteResource(resource);
|
||||||
|
|
||||||
let description: string = '';
|
let description = '';
|
||||||
switch (resource.type) {
|
switch (resource.type) {
|
||||||
case 'branch':
|
case 'branch':
|
||||||
description = `$(git-branch) ${resource.branch}`;
|
description = `$(git-branch) ${resource.branch}`;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -16,7 +16,7 @@ export class OpenStatusFileCommandQuickPickItem extends OpenFileCommandQuickPick
|
|||||||
directory = '';
|
directory = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = (status.status === 'R' && status.originalFileName)
|
const description = (status.status === 'R' && status.originalFileName)
|
||||||
? `${directory} \u00a0\u2190\u00a0 ${status.originalFileName}`
|
? `${directory} \u00a0\u2190\u00a0 ${status.originalFileName}`
|
||||||
: directory;
|
: directory;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export class OpenStatusFilesCommandQuickPickItem extends CommandQuickPickItem {
|
|||||||
super(item || {
|
super(item || {
|
||||||
label: `$(file-symlink-file) Open Changed Files`,
|
label: `$(file-symlink-file) Open Changed Files`,
|
||||||
description: ''
|
description: ''
|
||||||
//detail: `Opens all of the changed files in the repository`
|
// detail: `Opens all of the changed files in the repository`
|
||||||
}, Commands.OpenChangedFiles, [
|
}, Commands.OpenChangedFiles, [
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -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));
|
||||||
@@ -211,7 +211,6 @@ export class RepoStatusQuickPick {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (goBackCommand) {
|
if (goBackCommand) {
|
||||||
items.splice(0, 0, goBackCommand);
|
items.splice(0, 0, goBackCommand);
|
||||||
}
|
}
|
||||||
|
|||||||