25 Commits

Author SHA1 Message Date
Eric Amodio
6b9fc3f9d2 Preps v4.0.0-alpha.2 2017-06-05 19:05:20 -04:00
Eric Amodio
5af70fd2bf Adds welcome message for first-time users 2017-06-05 19:04:29 -04:00
Eric Amodio
cecd3524b8 Major refactor/rework -- many new features and breaking changes
Adds all-new, beautiful, highly customizable and themeable, file blame annotations
Adds all-new configurability and themeability to the current line blame annotations
Adds all-new configurability to the status bar blame information
Adds all-new configurability over which commands are added to which menus via the `gitlens.advanced.menus` setting
Adds better configurability over where Git code lens will be shown -- both by default and per language
Adds an all-new `changes` (diff) hover annotation to the current line - provides instant access to the line's previous version
Adds `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) - toggles the current line blame annotations on and off
Adds `Show Line Blame Annotations` command (`gitlens.showLineBlame`) - shows the current line blame annotations
Adds `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) - toggles the file blame annotations on and off
Adds `Show File Blame Annotations` command (`gitlens.showFileBlame`) - shows the file blame annotations
Adds `Open File in Remote` command (`gitlens.openFileInRemote`) to the `editor/title` context menu
Adds `Open Repo in Remote` command (`gitlens.openRepoInRemote`) to the `editor/title` context menu
Changes the position of the `Open File in Remote` command (`gitlens.openFileInRemote`) in the context menus - now in the `navigation` group
Changes the `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) to always toggle the Git code lens on and off
Removes the on-demand `trailing` file blame annotations -- didn't work out and just ended up with a ton of visual noise
Removes `Toggle Blame Annotations` command (`gitlens.toggleBlame`) - replaced by the `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`)
Removes `Show Blame Annotations` command (`gitlens.showBlame`) - replaced by the `Show File Blame Annotations` command (`gitlens.showFileBlame`)
2017-06-05 19:04:18 -04:00
Eric Amodio
5298511bb9 Preps v3.6.0 2017-06-02 18:45:04 -04:00
Eric Amodio
4400ab1da9 Updates dependencies 2017-06-02 15:12:57 -04:00
Eric Amodio
6a9977b954 Fixes issue where the wrong diff line could be shown 2017-06-02 15:12:31 -04:00
Eric Amodio
c0d5f55baa Preps v3.6.0-beta 2017-05-27 04:33:27 -04:00
Eric Amodio
68f6ae8f3a Updates dependencies 2017-05-27 04:32:42 -04:00
Eric Amodio
53c691898f Changes behavior of diffWithWorking to always does what it says
Compares the current file with the working tree -- if the current file *is* the working file, it will show a `File matches the working tree` message
2017-05-27 04:26:51 -04:00
Eric Amodio
365af9c54b Changes behavior of diffWithPrevious to always does what it says
Compares the current file with the previous commit to that file
2017-05-27 04:26:12 -04:00
Eric Amodio
55b1a66ec0 Adds 'gitlens.diffWithWorking' status bar command option
Changes 'gitlens.diffWithPrevious' status bar command option behavior
2017-05-27 04:25:12 -04:00
Eric Amodio
522e5a49a2 Renames 'Compare with *' in quick pick menus
Renames 'Compare File with Previous Commit' command
Renames 'Compare Line with Previous Commit' command
Renames 'Compare Line with Working Tree' command
2017-05-27 04:20:25 -04:00
Eric Amodio
e99febb52d Adds diff info to active line hover always 2017-05-27 02:39:10 -04:00
Eric Amodio
2036c8abaf Preps v3.5.1 2017-05-25 01:39:16 -04:00
Eric Amodio
021a5b833a Fixes #71 - blame invalid on external edit 2017-05-25 01:31:24 -04:00
Eric Amodio
f1042de9c7 Stops some code lens actions when uncommitted 2017-05-25 01:30:28 -04:00
Eric Amodio
efd3d40aa8 Switches to use GitUris in more places 2017-05-25 01:05:18 -04:00
Eric Amodio
9c7062020e Fixes issue with blame on versioned files
Stops falling back to the cached blame of the working file
Handles git scheme urls properly
2017-05-25 00:49:36 -04:00
Eric Amodio
9da80c121b Debounces other active line events 2017-05-25 00:35:12 -04:00
Eric Amodio
5380724323 Preps v3.5.0 2017-05-24 23:01:28 -04:00
Eric Amodio
77651701aa Removes insiders flag from stash commands 2017-05-24 22:59:41 -04:00
Eric Amodio
bb834f2e0a Removes insiders flag from remotes 2017-05-24 22:58:54 -04:00
Eric Amodio
535e627048 Preps v3.5.0-beta.2 2017-05-24 13:02:02 -04:00
Eric Amodio
8a74950708 Fixes #40 - encoding issues
This is only a partial fix, as vscode doesn't allow enough controls to fix everything
2017-05-24 12:52:47 -04:00
Eric Amodio
3502bdf6c7 Adds more linting rules
Fixes lint issues
2017-05-23 18:48:04 -04:00
70 changed files with 5039 additions and 1759 deletions

21
.vscode/tasks.json vendored
View File

@@ -9,24 +9,33 @@
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "2.0.0",
"showOutput": "always",
"showOutput": "silent",
"tasks": [
{
"taskName": "compile",
"command": "npm run compile",
"command": "npm run compile --silent",
"isBuildCommand": true,
"isShellCommand": true,
"problemMatcher": [ "$tsc", "$tslint5" ]
"problemMatcher": [
"$tsc",
{
"base": "$tslint5",
"fileLocation": "relative"
}
]
},
{
"taskName": "lint",
"command": "npm run lint",
"command": "npm run lint --silent",
"isShellCommand": true,
"problemMatcher": "$tslint5"
"problemMatcher": {
"base": "$tslint5",
"fileLocation": "relative"
}
},
{
"taskName": "watch",
"command": "npm run watch",
"command": "npm run watch --silent",
"isBackground": true,
"isShellCommand": true,
"problemMatcher": "$tsc-watch"

View File

@@ -4,20 +4,82 @@ 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/).
## [3.5.0-beta] - 2017-05-23
## [4.0.0-alpha.2] - 2017-06-05
### Added
- Adds all-new, beautiful, highly customizable and themeable, file blame annotations
- Can now fully customize the [layout and content](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#file-blame-annotation-settings), as well as the [theme](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#theme-settings)
- Adds all-new configurability and themeability to the current line blame annotations
- Can now fully customize the [layout and content](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#line-blame-annotation-settings), as well as the [theme](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#theme-settings)
- Adds all-new configurability to the status bar blame information
- Can now fully customize the [layout and content](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#status-bar-settings)
- Adds all-new [configurability](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#advanced-settings) over which commands are added to which menus via the `gitlens.advanced.menus` setting
- Adds better [configurability](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#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
### Changed
- (BREAKING) Almost all of the GitLens settings have either been renamed, removed, or otherwise changed - see the [README](https://github.com/eamodio/vscode-gitlens/blob/develop/README.md#extension-settings)`
- 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
### 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`)
## [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
- Improves performance
- Reduces the number of git calls on known "untrackables"
- 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
- Adds diff information (previous line's code) 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 diff information (the line's previous version) into the active line hover when the current line is uncommitted
- Adds `gitlens.statusBar.alignment` settings to control the alignment of the status bar -- thanks to [PR #72](https://github.com/eamodio/vscode-gitlens/pull/72) by Zack Schuster ([@zackschuster](https://github.com/zackschuster))!
- Adds `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 `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 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
- 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 `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
@@ -26,6 +88,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Fixed
- 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 command execution when using `"gitlens.statusBar.command": "gitlens.showQuickRepoHistory"`
- Fixes a bunch of issues that were revealed by enabling Typescript `strict` mode

196
README.md
View File

@@ -5,56 +5,68 @@
# 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
#### Featuring code lens, whole file inline blame annotations, and navigation and exploration via quick pick menus
#### Featuring code lens, file blame annotations, and navigation and exploration via quick pick menus
![GitLens preview 1](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview1.gif)
#### Featuring selected line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more
#### Featuring current line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more
![GitLens preview 2](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview2.gif)
## Features
#### Git Blame Annotations
- Adds a **blame annotation** to the end of the selected line showing the commit id and message, with more details 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)
- 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 a `Toggle Blame Annotations` command (`gitlens.toggleBlame`) with a shortcut of `alt+b` to toggle **inline Git blame annotations** for a whole file with multiple styles — compact, expanded, and trailing
- Also adds a `Show Blame Annotations` command (`gitlens.showBlame`)
- Adds on-demand, beautiful, highly [customizable](#file-blame-annotation-settings) and [themeable](#theme-settings), **Git blame annotations** of the whole file
- Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings)
- Contains the commit message and date, by [default](#file-blame-annotation-settings)
- 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 **author and date blame information** about the selected line to the **status bar** ([optional](#extension-settings), on by default)
- By default clicking on the status bar shows a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Provides [customizable](#extension-settings) click behavior of the status bar — choose between one of the following
- Toggle whole file blame annotations on and off
- Toggle code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set
- Compare the file with the previous commit
- Show a quick pick menu with details and commands for the commit
- Adds [customizable](#status-bar-settings) **blame information** about the current line to the **status bar** ([optional](#status-bar-settings), on by default)
- Contains the commit author and date, by [default](#status-bar-settings)
- Clicking the status bar item will, by [default](#status-bar-settings), show a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Provides [customizable](#status-bar-settings) click behavior — choose between one of the following
- Toggle file blame annotations on and off
- Toggle code lens on and off
- Compare the line commit with the previous commit
- Compare the line commit with the working tree
- Show a quick pick menu with details and commands for the commit (default)
- Show a quick pick menu with file details and commands for the commit
- Show a quick pick menu with the commit history of the file
- Show a quick pick menu with the commit history of the current branch
- Adds a `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the file blame annotations on and off
- Also adds a `Show File Blame Annotations` command (`gitlens.showFileBlame`)
- Adds a `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) to toggle the current line blame annotations on and off
- Also adds a `Show Line Blame Annotations` command (`gitlens.showLineBlame`)
#### Git Code Lens
- Adds **code lens** to the top of the file and on code blocks ([optional](#extension-settings), on by default)
- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default)
- **Recent Change** — author and date of the most recent commit for the file or code block
- By default, clicking on the code lens shows a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Clicking the code lens will, by [default](#code-lens-settings), show a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- **Authors** — number of authors of the file or code block and the most prominent author (if there is more than one)
- By default, clicking on the code lens toggles the inline Git blame annotations on and off for the whole file
- Clicking the code lens will, by [default](#code-lens-settings), toggle the file Git blame annotations on and off of the whole file
- Will be hidden if the author of the most recent commit is also the only author of the file or block, to avoid duplicate information and reduce visual noise
- Provides [customizable](#extension-settings) click behavior for each code lens — choose between one of the following
- Toggle whole file blame annotations on and off
- Compare the file with the previous commit
- Provides [customizable](#code-lens-settings) click behavior for each code lens — choose between one of the following
- Toggle file blame annotations on and off
- Compare the commit with the previous commit
- Show a quick pick menu with details and commands for the commit
- Show a quick pick menu with file details and commands for the commit
- Show a quick pick menu with the commit history of the file
- Show a quick pick menu with the commit history of the current branch
- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set
- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off
#### Powerful Comparison Tools
@@ -68,13 +80,13 @@ 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 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 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
@@ -120,7 +132,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
![Line Commit Details Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-commit-file-details.png)
- Provides entries to `Show Commit Details`, `Show File History`, `Compare with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more
- Provides entries to `Show Commit Details`, `Show File History`, `Compare File with...`, `Copy to Clipboard`, `Open File`, `Open File in <remote-service>` when available, and more
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- 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
@@ -141,7 +153,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
![Stashed Changes Quick Pick Menu](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/screenshot-stash-list.png)
- [Insiders only](#insiders) — Provides entries to `Stash Changes`
- Provides entries to `Stash Changes`
- Navigate back to the previous quick pick menu via `alt+left arrow`, if available
- Chosing a stash entry shows a **stash details quick pick menu** which is very similar to the **commit details quick pick menu** above
@@ -150,7 +162,7 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
- 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`
- [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
- 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
@@ -174,48 +186,124 @@ 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
- [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
Add [`"gitlens.insiders": true`](#extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues.
Add [`"gitlens.insiders": true`](#general-extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues.
## Extension Settings
GitLens is highly customizable and provides many configuration settings to allow the personalization of almost all features
### General Settings
|Name | Description
|-----|------------
|`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features
|`gitlens.outputLevel`|Specifies how much (if any) output will be sent to the GitLens output channel
|`gitlens.blame.annotation.activeLine`|Specifies whether and how to show blame annotations on the active line. `off` - no annotation. `inline` - adds a trailing annotation to the active line. `hover` - adds hover annotation to the active line. `both` - adds both `inline` and `hover` annotations
|`gitlens.blame.annotation.activeLineDarkColor`|Specifies the color of the active line blame annotation to use with a dark theme. Must be a valid css color
|`gitlens.blame.annotation.activeLineLightColor`|Specifies the color of the active line blame annotation to use with a light theme. Must be a valid css color
|`gitlens.blame.annotation.highlight`|Specifies whether and how to highlight blame annotations. `none` - no highlight. `gutter` - adds a gutter icon. `line` - adds a full-line highlight. `both` - adds both `gutter` and `line` highlights
|`gitlens.blame.annotation.style`|Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation on every line
|`gitlens.blame.annotation.author`|Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.date`|Specifies whether and how the commit date will be shown in the blame annotations. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.blame.annotation.dateFormat`. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.blame.annotation.message`|Specifies whether the commit message will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.sha`|Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.codeLens.visibility`|Specifies when code lens will be shown in the active document. `auto` - always shown. `ondemand` - never shown, unless toggled via the `gitlens.toggleCodeLens` command. `off` - never shown
|`gitlens.codeLens.authors.enabled`|Specifies whether the authors code lens is shown
|`gitlens.codeLens.authors.command`|Specifies the command executed when the authors code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.recentChange.enabled`|Specifies whether the recent change code lens is shown
|`gitlens.codeLens.recentChange.command`|"Specifies the command executed when the recent change code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.location`|Specifies where code lens will be rendered in the active document. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `gitlens.codeLens.locationCustomSymbols`
|`gitlens.codeLens.locationCustomSymbols`|Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind`
|`gitlens.codeLens.languageLocations`|Specifies where code lens will be rendered in the active document for the specified languages
|`gitlens.menus.diff.enabled`|Specifies whether diff commands will be added to the context menus
|`gitlens.statusBar.enabled`|Specifies whether blame information is shown in the status bar
### Blame Annotation Settings
#### File Blame Annotation Settings
|Name | Description
|-----|------------
|`gitlens.blame.file.annotationType`|Specifies the type of blame annotations that will be shown for the current file. `gutter` - adds an annotation to the beginning of each line. `hover` - shows annotations when hovering over each line
|`gitlens.blame.file.lineHighlight.enabled`|Specifies whether or not to highlight lines associated with the current line
|`gitlens.blame.file.lineHighlight.locations`|Specifies where the associated line highlights will be shown. `gutter` - adds a gutter glyph. `line` - adds a full-line highlight background color. `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`gitlens.annotations.file.gutter.format`|Specifies the format of the gutter blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.file.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.annotations.file.gutter.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.annotations.file.gutter.compact`|Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations
|`gitlens.annotations.file.gutter.heatmap.enabled`|Specifies whether or not to provide a heatmap indicator in the gutter blame annotations
|`gitlens.annotations.file.gutter.heatmap.location`|Specifies where the heatmap indicators will be shown in the gutter blame annotations. `left` - adds a heatmap indicator on the left edge of the gutter blame annotations. `right` - adds a heatmap indicator on the right edge of the gutter blame annotations
|`gitlens.annotations.file.gutter.hover.details`|Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations
|`gitlens.annotations.file.gutter.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.annotations.file.hover.heatmap.enabled`|Specifies whether or not to provide heatmap indicators on the left edge of each line
|`gitlens.annotations.file.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
#### Line Blame Annotation Settings
|Name | Description
|-----|------------
|`gitlens.blame.line.enabled`|Specifies whether or not to provide a blame annotation for the current line
|`gitlens.blame.line.annotationType`|Specifies the type of blame annotations that will be shown for the current line. `trailing` - adds an annotation to the end of the current line. `hover` - shows annotations when hovering over the current line
|`gitlens.annotations.line.trailing.format`|Specifies the format of the trailing blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.currentLine.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.annotations.line.trailing.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.annotations.line.trailing.hover.details`|Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations
|`gitlens.annotations.line.trailing.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations
|`gitlens.annotations.line.trailing.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.annotations.line.hover.details`|Specifies whether or not to provide a commit details hover annotation for the current line
|`gitlens.annotations.line.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation for the current line
### Code Lens Settings
|Name | Description
|-----|------------
|`gitlens.codeLens.enabled`|Specifies whether or not to provide any Git code lens
|`gitlens.codeLens.recentChange.enabled`|Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block
|`gitlens.codeLens.recentChange.command`|Specifies the command to be executed when the `recent change` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.authors.enabled`|Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)
|`gitlens.codeLens.authors.command`|Specifies the command to be executed when the `authors` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.locations`|Specifies where Git code lens will be shown in the document. `document` - adds code lens at the top of the document. `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc). `blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines. `custom` - adds code lens at the start of symbols contained in `gitlens.codeLens.locationCustomSymbols`
|`gitlens.codeLens.customLocationSymbols`|Specifies the set of document symbols where Git code lens will be shown in the document
|`gitlens.codeLens.perLanguageLocations`|Specifies where Git code lens will be shown in the document for the specified languages
### Status Bar Settings
|Name | Description
|-----|------------
|`gitlens.statusBar.enabled`|Specifies whether or not to provide blame information on the status bar
|`gitlens.statusBar.alignment`|Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right
|`gitlens.statusBar.command`|"Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current 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`
|`gitlens.statusBar.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.statusBar.format`|Specifies the format of the blame information on the status bar. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
|`gitlens.statusBar.command`|Specifies the command to be executed when the blame status bar item is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.statusBar.format`|Specifies the format of the status bar blame information. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.statusBar.dateFormat`|Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats
### Theme Settings
|Name | Description
|-----|------------
|`gitlens.theme.annotations.file.gutter.separateLines`|Specifies whether or not gutter blame annotations will be separated by a small gap
|`gitlens.theme.annotations.file.gutter.dark.backgroundColor`|Specifies the dark theme background color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.backgroundColor`|Specifies the light theme background color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.dark.foregroundColor`|Specifies the dark theme foreground color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.foregroundColor`|Specifies the light theme foreground color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor`|Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor`|Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations
|`gitlens.theme.annotations.file.hover.separateLines`|Specifies whether or not hover blame annotations will be separated by a small gap (if heatmap is enabled)
|`gitlens.theme.annotations.line.trailing.dark.backgroundColor`|Specifies the dark theme background color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.light.backgroundColor`|Specifies the light theme background color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.dark.foregroundColor`|Specifies the dark theme foreground color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.light.foregroundColor`|Specifies the light theme foreground color of the trailing blame annotation
|`gitlens.theme.lineHighlight.dark.backgroundColor`|Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color
|`gitlens.theme.lineHighlight.light.backgroundColor`|Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color
|`gitlens.theme.lineHighlight.dark.overviewRulerColor`|Specifies the dark theme overview ruler color of the associated line highlights in blame annotations
|`gitlens.theme.lineHighlight.light.overviewRulerColor`|Specifies the light theme overview ruler color of the associated line highlights in blame annotations
### Advanced Settings
|Name | Description
|-----|------------
|`gitlens.advanced.toggleWhitespace.enabled`|Specifies whether or not to toggle whitespace off then showing blame annotations (*may* be required by certain fonts/themes)
|`gitlens.advanced.menus`|Specifies which commands will be added to 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
- 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
## 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)

View File

@@ -1,6 +1,6 @@
<?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">
<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>
</svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<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>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 313 B

1953
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -56,5 +56,4 @@ export class ActiveEditorTracker extends Disposable {
this._resolver = undefined;
return editor;
}
}
}

View File

@@ -0,0 +1,282 @@
'use strict';
import { Functions, Objects } from '../system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { TextDocumentComparer, TextEditorComparer } from '../comparers';
import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
import { Logger } from '../logger';
import { WhitespaceController } from './whitespaceController';
export const Decorations = {
annotation: window.createTextEditorDecorationType({
isWholeLine: true
} as DecorationRenderOptions),
highlight: undefined as TextEditorDecorationType | undefined
};
export class AnnotationController extends Disposable {
private _onDidToggleAnnotations = new EventEmitter<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');
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().get<IConfig>(ExtensionKey)!;
const cfgHighlight = cfg.blame.file.lineHighlight;
const cfgTheme = cfg.theme.lineHighlight;
let changed = false;
if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) ||
!Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) {
changed = true;
Decorations.highlight && Decorations.highlight.dispose();
if (cfgHighlight.enabled) {
Decorations.highlight = window.createTextEditorDecorationType({
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
dark: {
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor
: undefined,
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-dark.svg')
: undefined,
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor
: undefined
},
light: {
backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line)
? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor
: undefined,
gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter)
? this.context.asAbsolutePath('images/blame-light.svg')
: undefined,
overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler)
? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor
: undefined
}
});
}
else {
Decorations.highlight = undefined;
}
}
if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) ||
!Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) ||
!Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) {
changed = true;
}
this._config = cfg;
if (changed) {
// Since the configuration has changed -- reset any visible annotations
for (const provider of this._annotationProviders.values()) {
if (provider === undefined) continue;
provider.reset();
}
}
}
async clear(column: number) {
const provider = this._annotationProviders.get(column);
if (!provider) return;
this._annotationProviders.delete(column);
await provider.dispose();
if (this._annotationProviders.size === 0) {
Logger.log(`Remove listener registrations for annotations`);
this._annotationsDisposable && this._annotationsDisposable.dispose();
this._annotationsDisposable = undefined;
}
this._onDidToggleAnnotations.fire();
}
getAnnotationType(editor: TextEditor): FileAnnotationType | undefined {
const provider = this.getProvider(editor);
return provider === undefined ? undefined : provider.annotationType;
}
getProvider(editor: TextEditor): AnnotationProviderBase | undefined {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined;
return this._annotationProviders.get(editor.viewColumn || -1);
}
async showAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise<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;
// We have to defer because isDirty is not reliable inside this event
setTimeout(() => {
// If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
if (e.document.isDirty) return;
// If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
// Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking
Logger.log('TextDocumentChanged:', `Clear annotations for column ${key}`);
this.clear(key);
}, 1);
}
}
private _onTextDocumentClosed(e: TextDocument) {
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e)) continue;
Logger.log('TextDocumentClosed:', `Clear annotations for column ${key}`);
this.clear(key);
}
}
private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
const viewColumn = e.viewColumn || -1;
Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${viewColumn}`);
await this.clear(viewColumn);
for (const [key, p] of this._annotationProviders) {
if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;
Logger.log('TextEditorViewColumnChanged:', `Clear annotations for column ${key}`);
await this.clear(key);
}
}
private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;
for (const [key, p] of this._annotationProviders) {
if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;
Logger.log('VisibleTextEditorsChanged:', `Clear annotations for column ${key}`);
this.clear(key);
}
}
}

View File

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

View File

@@ -0,0 +1,189 @@
import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode';
import { IThemeConfig, themeDefaults } from '../configuration';
import { CommitFormatter, GitCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
interface IHeatmapConfig {
enabled: boolean;
location?: 'left' | 'right';
}
interface IRenderOptions {
uncommittedForegroundColor?: {
dark: string;
light: string;
};
before?: DecorationInstanceRenderOptions & ThemableDecorationRenderOptions & { height?: string };
dark?: DecorationInstanceRenderOptions;
light?: DecorationInstanceRenderOptions;
}
export const endOfLineIndex = 1000000;
export class Annotations {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) {
const color = this._getHeatmapColor(now, date);
(decoration.renderOptions!.before! as any).borderColor = color;
}
private static _getHeatmapColor(now: moment.Moment, date: Date) {
const days = now.diff(moment(date), 'days');
if (days <= 2) return '#ffeca7';
if (days <= 7) return '#ffdd8c';
if (days <= 14) return '#ffdd7c';
if (days <= 30) return '#fba447';
if (days <= 60) return '#f68736';
if (days <= 90) return '#f37636';
if (days <= 180) return '#ca6632';
if (days <= 365) return '#c0513f';
if (days <= 730) return '#a2503a';
return '#793738';
}
static async changesHover(commit: GitCommit, line: number, uri: GitUri, git: GitService): Promise<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,
margin: '0 26px 0 0'
}
},
dark: {
before: commit.isUncommitted
? { ...renderOptions.dark, ...{ color: renderOptions.uncommittedForegroundColor!.dark } }
: { ...renderOptions.dark }
},
light: {
before: commit.isUncommitted
? { ...renderOptions.light, ...{ color: renderOptions.uncommittedForegroundColor!.light } }
: { ...renderOptions.light }
}
} as DecorationInstanceRenderOptions
} as DecorationOptions;
}
static gutterRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
const cfgFileTheme = cfgTheme.annotations.file.gutter;
let borderStyle = undefined;
let borderWidth = undefined;
if (heatmap.enabled) {
borderStyle = 'solid';
borderWidth = heatmap.location === 'left' ? '0 0 0 2px' : '0 2px 0 0';
}
return {
uncommittedForegroundColor: {
dark: cfgFileTheme.dark.uncommittedForegroundColor || cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor,
light: cfgFileTheme.light.uncommittedForegroundColor || cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor
},
before: {
borderStyle: borderStyle,
borderWidth: borderWidth,
height: cfgFileTheme.separateLines ? 'calc(100% - 1px)' : '100%'
},
dark: {
backgroundColor: cfgFileTheme.dark.backgroundColor || undefined,
color: cfgFileTheme.dark.foregroundColor || themeDefaults.annotations.file.gutter.dark.foregroundColor
} as DecorationInstanceRenderOptions,
light: {
backgroundColor: cfgFileTheme.light.backgroundColor || undefined,
color: cfgFileTheme.light.foregroundColor || themeDefaults.annotations.file.gutter.light.foregroundColor
} as DecorationInstanceRenderOptions
};
}
static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean): DecorationOptions {
return {
hoverMessage: CommitFormatter.toHoverAnnotation(commit),
renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined
} as DecorationOptions;
}
static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {
if (!heatmap.enabled) return { before: undefined };
return {
before: {
borderStyle: 'solid',
borderWidth: '0 0 0 2px',
contentText: '\u200B',
height: cfgTheme.annotations.file.hover.separateLines ? 'calc(100% - 1px)' : '100%',
margin: '0 26px 0 0'
}
} as IRenderOptions;
}
static trailing(commit: GitCommit, format: string, dateFormat: string | null, cfgTheme: IThemeConfig): DecorationOptions {
const message = CommitFormatter.fromTemplate(format, commit, dateFormat);
return {
renderOptions: {
after: {
contentText: `\u00a0${message}\u00a0`
},
dark: {
after: {
backgroundColor: cfgTheme.annotations.line.trailing.dark.backgroundColor || undefined,
color: cfgTheme.annotations.line.trailing.dark.foregroundColor || themeDefaults.annotations.line.trailing.dark.foregroundColor
}
},
light: {
after: {
backgroundColor: cfgTheme.annotations.line.trailing.light.backgroundColor || undefined,
color: cfgTheme.annotations.line.trailing.light.foregroundColor || themeDefaults.annotations.line.trailing.light.foregroundColor
}
}
} as DecorationInstanceRenderOptions
} as DecorationOptions;
}
static withRange(decoration: DecorationOptions, start?: number, end?: number): DecorationOptions {
let range = decoration.range;
if (start !== undefined) {
range = range.with({
start: range.start.with({ character: start })
});
}
if (end !== undefined) {
range = range.with({
end: range.end.with({ character: end })
});
}
return { ...decoration, ...{ range: range } };
}
}

View File

@@ -0,0 +1,82 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { GitService, GitUri, IGitBlame } from '../gitService';
import { WhitespaceController } from './whitespaceController';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
protected _blame: Promise<IGitBlame>;
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) {
super(context, editor, decoration, highlightDecoration, whitespaceController);
this._blame = this.git.getBlameForFile(this.uri);
}
async selection(shaOrLine?: string | number, blame?: IGitBlame) {
if (!this.highlightDecoration) return;
if (blame === undefined) {
blame = await this._blame;
if (!blame || !blame.lines.length) return;
}
const offset = this.uri.offset;
let sha: string | undefined = undefined;
if (typeof shaOrLine === 'string') {
sha = shaOrLine;
}
else if (typeof shaOrLine === 'number') {
const line = shaOrLine - offset;
if (line >= 0) {
const commitLine = blame.lines[line];
sha = commitLine && commitLine.sha;
}
}
else {
sha = Iterables.first(blame.commits.values()).sha;
}
if (!sha) {
this.editor.setDecorations(this.highlightDecoration, []);
return;
}
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)));
this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
}
async validate(): Promise<boolean> {
const blame = await this._blame;
return blame !== undefined && blame.lines.length !== 0;
}
protected async getBlame(requiresWhitespaceHack: boolean): Promise<IGitBlame | 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: IGitBlame;
if (whitespacePromise) {
[blame] = await Promise.all([this._blame, whitespacePromise]);
}
else {
blame = await this._blame;
}
if (!blame || !blame.lines.length) {
this.whitespaceController && await this.whitespaceController.restore();
return undefined;
}
return blame;
}
}

View 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.currentStart - 2;
for (const change of chunk.current) {
if (change === undefined) continue;
count++;
if (change.state === 'unchanged') continue;
decorators.push({
range: new Range(new Position(count, 0), new Position(count, 0))
} as DecorationOptions);
}
}
this.editor.setDecorations(this.decoration, decorators);
return true;
}
async selection(shaOrLine?: string | number): Promise<void> {
}
async validate(): Promise<boolean> {
return true;
}
}

View 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;
}
}

View 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;
}
}

View File

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

View File

@@ -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)}\` &nbsp; __Uncommitted change__\n\n\---\n\`\`\`\n${original}\n\`\`\``;
}
// else {
// hoverMessage = `\`${'0'.repeat(8)}\` &nbsp; __Uncommitted change__\n\n\`Added\``;
// }
}
}
}
let decorationOptions: [DecorationOptions] | undefined = undefined;
switch (activeLine) {
case 'both':
case 'inline':
const range = editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000));
decorationOptions = [
{
range: range.with({
start: range.start.with({
character: range.end.character
})
}),
hoverMessage: hoverMessage,
renderOptions: {
after: {
contentText: annotation
},
dark: {
after: {
color: this._config.blame.annotation.activeLineDarkColor || 'rgba(153, 153, 153, 0.35)'
}
},
light: {
after: {
color: this._config.blame.annotation.activeLineLightColor || 'rgba(153, 153, 153, 0.35)'
}
}
} as DecorationInstanceRenderOptions
} as DecorationOptions
];
if (activeLine === 'both') {
// Add a hover decoration to the area between the start of the line and the first non-whitespace character
decorationOptions.push({
range: range.with({
end: range.end.with({
character: editor.document.lineAt(range.end.line).firstNonWhitespaceCharacterIndex
})
}),
hoverMessage: hoverMessage
} as DecorationOptions);
}
break;
case 'hover':
decorationOptions = [
{
range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)),
hoverMessage: hoverMessage
} as DecorationOptions
];
break;
}
if (decorationOptions !== undefined) {
editor.setDecorations(activeLineDecoration, decorationOptions);
}
}
}
}

View File

@@ -1,253 +0,0 @@
'use strict';
import { Functions } from './system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationProvider } from './blameAnnotationProvider';
import { TextDocumentComparer, TextEditorComparer } from './comparers';
import { IBlameConfig } from './configuration';
import { ExtensionKey } from './constants';
import { BlameabilityChangeEvent, 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);
}
}
}

View File

@@ -1,113 +0,0 @@
'use strict';
import { IBlameConfig } from './configuration';
import { GitCommit, IGitCommitLine } from './gitService';
import * as moment from 'moment';
export const defaultAbsoluteDateLength = 10;
export const defaultRelativeDateLength = 13;
export const defaultAuthorLength = 16;
export const defaultMessageLength = 32;
export enum BlameAnnotationFormat {
Constrained,
Unconstrained
}
export class BlameAnnotationFormatter {
static getAnnotation(config: IBlameConfig, commit: GitCommit, format: BlameAnnotationFormat) {
const sha = commit.shortSha;
let message = this.getMessage(config, commit, format === BlameAnnotationFormat.Unconstrained ? 0 : defaultMessageLength);
if (format === BlameAnnotationFormat.Unconstrained) {
const authorAndDate = this.getAuthorAndDate(config, commit, config.annotation.dateFormat || 'MMMM Do, YYYY h:MMa');
if (config.annotation.sha) {
message = `${sha}${(authorAndDate ? `\u00a0\u2022\u00a0${authorAndDate}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`;
}
else if (config.annotation.author || config.annotation.date) {
message = `${authorAndDate}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`;
}
return message;
}
const author = this.getAuthor(config, commit, defaultAuthorLength);
const date = this.getDate(config, commit, config.annotation.dateFormat || 'MM/DD/YYYY', true);
if (config.annotation.sha) {
message = `${sha}${(author ? `\u00a0\u2022\u00a0${author}` : '')}${(date ? `\u00a0\u2022\u00a0${date}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`;
}
else if (config.annotation.author) {
message = `${author}${(date ? `\u00a0\u2022\u00a0${date}` : '')}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`;
}
else if (config.annotation.date) {
message = `${date}${(message ? `\u00a0\u2022\u00a0${message}` : '')}`;
}
return message;
}
static getAnnotationHover(config: IBlameConfig, line: IGitCommitLine, commit: GitCommit): string | Array<string> {
const message = `> \`${commit.message.replace(/\n/g, '\`\n>\n> \`')}\``;
if (commit.isUncommitted) {
return `\`${'0'.repeat(8)}\` &nbsp; __Uncommitted change__`;
}
return `\`${commit.shortSha}\` &nbsp; __${commit.author}__, ${moment(commit.date).fromNow()} _(${moment(commit.date).format(config.annotation.dateFormat || 'MMMM Do, YYYY h:MMa')})_ \n\n${message}`;
}
static getAuthorAndDate(config: IBlameConfig, commit: GitCommit, format: string, force: boolean = false) {
if (!force && !config.annotation.author && (!config.annotation.date || config.annotation.date === 'off')) return '';
if (!config.annotation.author) {
return this.getDate(config, commit, format);
}
if (!config.annotation.date || config.annotation.date === 'off') {
return this.getAuthor(config, commit);
}
return `${this.getAuthor(config, commit)}, ${this.getDate(config, commit, format)}`;
}
static getAuthor(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) {
if (!force && !config.annotation.author) return '';
const author = commit.isUncommitted ? 'Uncommitted' : commit.author;
if (!truncateTo) return author;
if (author.length > truncateTo) {
return `${author.substring(0, truncateTo - 1)}\u2026`;
}
if (force) return author; // Don't pad when just asking for the value
return author + '\u00a0'.repeat(truncateTo - author.length);
}
static getDate(config: IBlameConfig, commit: GitCommit, format: string, truncate: boolean = false, force: boolean = false) {
if (!force && (!config.annotation.date || config.annotation.date === 'off')) return '';
const date = config.annotation.date === 'relative'
? moment(commit.date).fromNow()
: moment(commit.date).format(format);
if (!truncate) return date;
const truncateTo = config.annotation.date === 'relative' ? defaultRelativeDateLength : defaultAbsoluteDateLength;
if (date.length > truncateTo) {
return `${date.substring(0, truncateTo - 1)}\u2026`;
}
if (force) return date; // Don't pad when just asking for the value
return date + '\u00a0'.repeat(truncateTo - date.length);
}
static getMessage(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) {
if (!force && !config.annotation.message) return '';
let message = commit.isUncommitted ? 'Uncommitted change' : commit.message;
if (truncateTo && message.length > truncateTo) {
return `${message.substring(0, truncateTo - 1)}\u2026`;
}
return message;
}
}

View File

@@ -1,302 +0,0 @@
'use strict';
import { Iterables } from './system';
import { DecorationInstanceRenderOptions, DecorationOptions, Disposable, ExtensionContext, Range, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationFormat, BlameAnnotationFormatter, defaultAuthorLength } from './blameAnnotationFormatter';
import { BlameDecorations } from './blameAnnotationController';
import { TextDocumentComparer } from './comparers';
import { BlameAnnotationStyle, IBlameConfig } from './configuration';
import { ExtensionKey } from './constants';
import { GitService, GitUri, IGitBlame } from './gitService';
import { WhitespaceController } from './whitespaceController';
export class BlameAnnotationProvider extends Disposable {
public document: TextDocument;
private _blame: Promise<IGitBlame>;
private _config: IBlameConfig;
private _disposable: Disposable;
constructor(context: ExtensionContext, private git: GitService, private whitespaceController: WhitespaceController | undefined, public editor: TextEditor, private uri: GitUri) {
super(() => this.dispose());
this.document = this.editor.document;
this._blame = this.git.getBlameForFile(this.uri);
this._config = workspace.getConfiguration(ExtensionKey).get<IBlameConfig>('blame')!;
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this));
this._disposable = Disposable.from(...subscriptions);
}
async dispose() {
if (this.editor) {
try {
this.editor.setDecorations(BlameDecorations.annotation, []);
BlameDecorations.highlight && this.editor.setDecorations(BlameDecorations.highlight, []);
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
if (BlameDecorations.highlight !== undefined) {
setTimeout(() => {
if (BlameDecorations.highlight === undefined) return;
this.editor.setDecorations(BlameDecorations.highlight, []);
}, 1);
}
}
catch (ex) { }
}
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace
this.whitespaceController && await this.whitespaceController.restore();
this._disposable && this._disposable.dispose();
}
private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent) {
if (!TextDocumentComparer.equals(this.document, e.textEditor && e.textEditor.document)) return;
return this.setSelection(e.selections[0].active.line);
}
async supportsBlame(): Promise<boolean> {
const blame = await this._blame;
return !!(blame && blame.lines.length);
}
async provideBlameAnnotation(shaOrLine?: string | number): Promise<boolean> {
let whitespacePromise: Promise<void> | undefined;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off)
if (this._config.annotation.style !== BlameAnnotationStyle.Trailing) {
whitespacePromise = this.whitespaceController && this.whitespaceController.override();
}
let blame: IGitBlame;
if (whitespacePromise) {
[blame] = await Promise.all([this._blame, whitespacePromise]);
}
else {
blame = await this._blame;
}
if (!blame || !blame.lines.length) {
this.whitespaceController && await this.whitespaceController.restore();
return false;
}
let blameDecorationOptions: DecorationOptions[] | undefined;
switch (this._config.annotation.style) {
case BlameAnnotationStyle.Compact:
blameDecorationOptions = this._getCompactGutterDecorations(blame);
break;
case BlameAnnotationStyle.Expanded:
blameDecorationOptions = this._getExpandedGutterDecorations(blame, false);
break;
case BlameAnnotationStyle.Trailing:
blameDecorationOptions = this._getExpandedGutterDecorations(blame, true);
break;
}
if (blameDecorationOptions) {
this.editor.setDecorations(BlameDecorations.annotation, blameDecorationOptions);
}
this._setSelection(blame, shaOrLine);
return true;
}
async setSelection(shaOrLine?: string | number) {
const blame = await this._blame;
if (!blame || !blame.lines.length) return;
return this._setSelection(blame, shaOrLine);
}
private _setSelection(blame: IGitBlame, shaOrLine?: string | number) {
if (!BlameDecorations.highlight) return;
const offset = this.uri.offset;
let sha: string | undefined = undefined;
if (typeof shaOrLine === 'string') {
sha = shaOrLine;
}
else if (typeof shaOrLine === 'number') {
const line = shaOrLine - offset;
if (line >= 0) {
const commitLine = blame.lines[line];
sha = commitLine && commitLine.sha;
}
}
else {
sha = Iterables.first(blame.commits.values()).sha;
}
if (!sha) {
this.editor.setDecorations(BlameDecorations.highlight, []);
return;
}
const highlightDecorationRanges = blame.lines
.filter(l => l.sha === sha)
.map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)));
this.editor.setDecorations(BlameDecorations.highlight, highlightDecorationRanges);
}
private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] {
const offset = this.uri.offset;
let count = 0;
let lastSha: string;
return blame.lines.map(l => {
const commit = blame.commits.get(l.sha);
if (commit === undefined) throw new Error(`Cannot find sha ${l.sha}`);
let color: string;
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
}
else {
color = l.previousSha ? '#999999' : '#6b6b6b';
}
let gutter = '';
if (lastSha !== l.sha) {
count = -1;
}
const isEmptyOrWhitespace = this.document.lineAt(l.line).isEmptyOrWhitespace;
if (!isEmptyOrWhitespace) {
switch (++count) {
case 0:
gutter = commit.shortSha;
break;
case 1:
gutter = `\u2759 ${BlameAnnotationFormatter.getAuthor(this._config, commit, defaultAuthorLength, true)}`;
break;
case 2:
gutter = `\u2759 ${BlameAnnotationFormatter.getDate(this._config, commit, this._config.annotation.dateFormat || 'MM/DD/YYYY', true, true)}`;
break;
default:
gutter = `\u2759`;
break;
}
}
const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit);
lastSha = l.sha;
return {
range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)),
hoverMessage: hoverMessage,
renderOptions: {
before: {
color: color,
contentText: gutter,
width: '11em'
}
}
} as DecorationOptions;
});
}
private _getExpandedGutterDecorations(blame: IGitBlame, trailing: boolean = false): DecorationOptions[] {
const offset = this.uri.offset;
let width = 0;
if (!trailing) {
if (this._config.annotation.sha) {
width += 5;
}
if (this._config.annotation.date && this._config.annotation.date !== 'off') {
if (width > 0) {
width += 7;
}
else {
width += 6;
}
if (this._config.annotation.date === 'relative') {
width += 2;
}
}
if (this._config.annotation.author) {
if (width > 5 + 6) {
width += 12;
}
else if (width > 0) {
width += 11;
}
else {
width += 10;
}
}
if (this._config.annotation.message) {
if (width > 5 + 6 + 10) {
width += 21;
}
else if (width > 5 + 6) {
width += 21;
}
else if (width > 0) {
width += 21;
}
else {
width += 19;
}
}
}
return blame.lines.map(l => {
const commit = blame.commits.get(l.sha);
if (commit === undefined) throw new Error(`Cannot find sha ${l.sha}`);
let color: string;
if (commit.isUncommitted) {
color = 'rgba(0, 188, 242, 0.6)';
}
else {
if (trailing) {
color = l.previousSha ? 'rgba(153, 153, 153, 0.5)' : 'rgba(107, 107, 107, 0.5)';
}
else {
color = l.previousSha ? 'rgb(153, 153, 153)' : 'rgb(107, 107, 107)';
}
}
const format = trailing ? BlameAnnotationFormat.Unconstrained : BlameAnnotationFormat.Constrained;
const gutter = BlameAnnotationFormatter.getAnnotation(this._config, commit, format);
const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit);
let renderOptions: DecorationInstanceRenderOptions;
if (trailing) {
renderOptions = {
after: {
color: color,
contentText: gutter
}
} as DecorationInstanceRenderOptions;
}
else {
renderOptions = {
before: {
color: color,
contentText: gutter,
width: `${width}em`
}
} as DecorationInstanceRenderOptions;
}
return {
range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)),
hoverMessage: hoverMessage,
renderOptions: renderOptions
} as DecorationOptions;
});
}
}

View File

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

View File

@@ -7,13 +7,13 @@ import { Telemetry } from '../telemetry';
export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' |
'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' |
'gitlens.openChangedFiles' | 'gitlens.openBranchInRemote' | 'gitlens.openCommitInRemote' | 'gitlens.openFileInRemote' | 'gitlens.openInRemote' | 'gitlens.openRepoInRemote' |
'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileHistory' |
'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' |
'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileBlame' | 'gitlens.showFileHistory' |
'gitlens.showLastQuickPick' | 'gitlens.showLineBlame' | 'gitlens.showQuickBranchHistory' |
'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' |
'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' |
'gitlens.showQuickRepoStatus' | 'gitlens.showQuickStashList' |
'gitlens.stashApply' | 'gitlens.stashDelete' | 'gitlens.stashSave' |
'gitlens.toggleBlame' | 'gitlens.toggleCodeLens';
'gitlens.toggleCodeLens' | 'gitlens.toggleFileBlame' | 'gitlens.toggleLineBlame';
export const Commands = {
CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands,
CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands,
@@ -31,7 +31,8 @@ export const Commands = {
OpenFileInRemote: 'gitlens.openFileInRemote' as Commands,
OpenInRemote: 'gitlens.openInRemote' as Commands,
OpenRepoInRemote: 'gitlens.openRepoInRemote' as Commands,
ShowBlame: 'gitlens.showBlame' as Commands,
ShowFileBlame: 'gitlens.showFileBlame' as Commands,
ShowLineBlame: 'gitlens.showLineBlame' as Commands,
ShowBlameHistory: 'gitlens.showBlameHistory' as Commands,
ShowCommitSearch: 'gitlens.showCommitSearch' as Commands,
ShowFileHistory: 'gitlens.showFileHistory' as Commands,
@@ -46,7 +47,8 @@ export const Commands = {
StashApply: 'gitlens.stashApply' as Commands,
StashDelete: 'gitlens.stashDelete' as Commands,
StashSave: 'gitlens.stashSave' as Commands,
ToggleBlame: 'gitlens.toggleBlame' as Commands,
ToggleFileBlame: 'gitlens.toggleFileBlame' as Commands,
ToggleLineBlame: 'gitlens.toggleLineBlame' as Commands,
ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands
};

View File

@@ -5,7 +5,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants';
import { GitService } from '../gitService';
import { Logger } from '../logger';
import { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks';
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
export interface DiffDirectoryCommandCommandArgs {
shaOrBranch1?: string;

View File

@@ -4,7 +4,7 @@ import { ActiveEditorCommand, Commands, getCommandUri } from './common';
import { BuiltInCommands } from '../constants';
import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger';
import { CommandQuickPickItem, BranchesQuickPick } from '../quickPicks';
import { BranchesQuickPick, CommandQuickPickItem } from '../quickPicks';
import * as path from 'path';
export interface DiffWithBranchCommandArgs {

View File

@@ -32,17 +32,15 @@ export class DiffWithPreviousCommand extends ActiveEditorCommand {
const gitUri = await GitUri.fromUri(uri, this.git);
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 log = await this.git.getLogForFile(gitUri.repoPath, gitUri.fsPath, undefined, sha ? undefined : 2, args.range!);
if (log === undefined) return window.showWarningMessage(`Unable to open compare. File is probably not under source control`);
args.commit = (sha && log.commits.get(sha)) || Iterables.first(log.commits.values());
// If the sha is missing, treat it as a DiffWithWorking
if (gitUri.sha === undefined) return commands.executeCommand(Commands.DiffWithWorking, uri, { commit: args.commit, showOptions: args.showOptions } as DiffWithWorkingCommandArgs);
}
catch (ex) {
Logger.error(ex, 'DiffWithPreviousCommand', `getLogForFile(${gitUri.repoPath}, ${gitUri.fsPath})`);

View File

@@ -26,6 +26,8 @@ export class DiffWithWorkingCommand extends ActiveEditorCommand {
if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) {
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 {
args.commit = await this.git.getLogCommit(gitUri.repoPath, gitUri.fsPath, gitUri.sha, { firstIfMissing: true });

View File

@@ -16,8 +16,11 @@ export const keys: Keys[] = [
'.'
];
export declare type KeyMapping = { [id: string]: (QuickPickItem | (() => Promise<QuickPickItem>) | undefined) };
let mappings: KeyMapping[] = [];
export declare interface KeyMapping {
[id: string]: (QuickPickItem | (() => Promise<QuickPickItem>) | undefined);
}
const mappings: KeyMapping[] = [];
let _instance: Keyboard;

View File

@@ -29,7 +29,7 @@ export class OpenInRemoteCommand extends ActiveEditorCommand {
return command.execute();
}
let placeHolder: string = '';
let placeHolder = '';
switch (args.resource.type) {
case 'branch':
// Check to see if the remote is in the branch

View File

@@ -1,28 +0,0 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { BlameAnnotationController } from '../blameAnnotationController';
import { Commands, EditorCommand } from './common';
import { Logger } from '../logger';
export interface ShowBlameCommandArgs {
sha?: string;
}
export class ShowBlameCommand extends EditorCommand {
constructor(private annotationController: BlameAnnotationController) {
super(Commands.ShowBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowBlameCommandArgs = {}): Promise<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
return this.annotationController.showBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line);
}
catch (ex) {
Logger.error(ex, 'ShowBlameCommand');
return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`);
}
}
}

View File

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

View 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`);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { BlameAnnotationController } from '../blameAnnotationController';
import { Commands, EditorCommand } from './common';
import { Logger } from '../logger';
export interface ToggleBlameCommandArgs {
sha?: string;
}
export class ToggleBlameCommand extends EditorCommand {
constructor(private annotationController: BlameAnnotationController) {
super(Commands.ToggleBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleBlameCommandArgs = {}): Promise<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
return this.annotationController.toggleBlameAnnotation(editor, args.sha !== undefined ? args.sha : editor.selection.active.line);
}
catch (ex) {
Logger.error(ex, 'ToggleBlameCommand');
return window.showErrorMessage(`Unable to show blame annotations. See output channel for more details`);
}
}
}

View File

@@ -0,0 +1,35 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController } 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`);
}
}
}

View 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`);
}
}
}

View File

@@ -2,31 +2,18 @@
import { Commands } from './commands';
import { OutputLevel } from './logger';
export type BlameAnnotationStyle = 'compact' | 'expanded' | 'trailing';
export const BlameAnnotationStyle = {
Compact: 'compact' as BlameAnnotationStyle,
Expanded: 'expanded' as BlameAnnotationStyle,
Trailing: 'trailing' as BlameAnnotationStyle
export { ExtensionKey } from './constants';
export type BlameLineHighlightLocations = 'gutter' | 'line' | 'overviewRuler';
export const BlameLineHighlightLocations = {
Gutter: 'gutter' as BlameLineHighlightLocations,
Line: 'line' as BlameLineHighlightLocations,
OverviewRuler: 'overviewRuler' as BlameLineHighlightLocations
};
export interface IBlameConfig {
annotation: {
style: BlameAnnotationStyle;
highlight: 'none' | 'gutter' | 'line' | 'both';
sha: boolean;
author: boolean;
date: 'off' | 'relative' | 'absolute';
dateFormat: string;
message: boolean;
activeLine: 'off' | 'inline' | 'hover' | 'both';
activeLineDarkColor: string;
activeLineLightColor: string;
};
}
export type CodeLensCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export type CodeLensCommand = 'gitlens.toggleFileBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export const CodeLensCommand = {
BlameAnnotate: Commands.ToggleBlame as CodeLensCommand,
BlameAnnotate: Commands.ToggleFileBlame as CodeLensCommand,
ShowBlameHistory: Commands.ShowBlameHistory as CodeLensCommand,
ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as CodeLensCommand,
@@ -36,49 +23,33 @@ export const CodeLensCommand = {
ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as CodeLensCommand
};
export type CodeLensLocation = 'all' | 'document+containers' | 'document' | 'custom' | 'none';
export const CodeLensLocation = {
All: 'all' as CodeLensLocation,
DocumentAndContainers: 'document+containers' as CodeLensLocation,
Document: 'document' as CodeLensLocation,
Custom: 'custom' as CodeLensLocation,
None: 'none' as CodeLensLocation
export type CodeLensLocations = 'document' | 'containers' | 'blocks' | 'custom';
export const CodeLensLocations = {
Document: 'document' as CodeLensLocations,
Containers: 'containers' as CodeLensLocations,
Blocks: 'blocks' as CodeLensLocations,
Custom: 'custom' as CodeLensLocations
};
export type CodeLensVisibility = 'auto' | 'ondemand' | 'off';
export const CodeLensVisibility = {
Auto: 'auto' as CodeLensVisibility,
OnDemand: 'ondemand' as CodeLensVisibility,
Off: 'off' as CodeLensVisibility
export type FileAnnotationType = 'gutter' | 'hover';
export const FileAnnotationType = {
Gutter: 'gutter' as FileAnnotationType,
Hover: 'hover' as FileAnnotationType
};
export interface ICodeLensConfig {
enabled: boolean;
command: CodeLensCommand;
}
export type LineAnnotationType = 'trailing' | 'hover';
export const LineAnnotationType = {
Trailing: 'trailing' as LineAnnotationType,
Hover: 'hover' as LineAnnotationType
};
export interface ICodeLensLanguageLocation {
language: string | undefined;
location: CodeLensLocation;
customSymbols?: string[];
}
export interface ICodeLensesConfig {
debug: boolean;
visibility: CodeLensVisibility;
location: CodeLensLocation;
locationCustomSymbols: string[];
languageLocations: ICodeLensLanguageLocation[];
recentChange: ICodeLensConfig;
authors: ICodeLensConfig;
}
export type StatusBarCommand = 'gitlens.toggleBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export type StatusBarCommand = 'gitlens.toggleFileBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.toggleCodeLens' | 'gitlens.diffWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory';
export const StatusBarCommand = {
BlameAnnotate: Commands.ToggleBlame as StatusBarCommand,
BlameAnnotate: Commands.ToggleFileBlame as StatusBarCommand,
ShowBlameHistory: Commands.ShowBlameHistory as StatusBarCommand,
ShowFileHistory: Commands.ShowFileHistory as CodeLensCommand,
DiffWithPrevious: Commands.DiffWithPrevious as StatusBarCommand,
DiffWithWorking: Commands.DiffWithWorking as StatusBarCommand,
ToggleCodeLens: Commands.ToggleCodeLens as StatusBarCommand,
ShowQuickCommitDetails: Commands.ShowQuickCommitDetails as StatusBarCommand,
ShowQuickCommitFileDetails: Commands.ShowQuickCommitFileDetails as StatusBarCommand,
@@ -86,26 +57,44 @@ export const StatusBarCommand = {
ShowQuickCurrentBranchHistory: Commands.ShowQuickCurrentBranchHistory as StatusBarCommand
};
export interface IStatusBarConfig {
enabled: boolean;
command: StatusBarCommand;
date: 'off' | 'relative' | 'absolute';
dateFormat: string;
alignment: 'left' | 'right';
}
export interface IAdvancedConfig {
caching: {
enabled: boolean;
statusBar: {
maxLines: number;
}
maxLines: number;
};
git: string;
gitignore: {
enabled: boolean;
};
maxQuickHistory: number;
menus: {
explorerContext: {
fileDiff: boolean;
history: boolean;
remote: boolean;
};
editorContext: {
blame: boolean;
copy: boolean;
details: boolean;
fileDiff: boolean;
history: boolean;
lineDiff: boolean;
remote: boolean;
};
editorTitle: {
blame: boolean;
fileDiff: boolean;
history: boolean;
status: boolean;
};
editorTitleContext: {
blame: boolean;
fileDiff: boolean;
history: boolean;
remote: boolean;
};
};
quickPick: {
closeOnFocusOut: boolean;
};
@@ -114,12 +103,192 @@ export interface IAdvancedConfig {
};
}
export interface ICodeLensLanguageLocation {
language: string | undefined;
locations: CodeLensLocations[];
customSymbols?: string[];
}
export interface IThemeConfig {
annotations: {
file: {
gutter: {
separateLines: boolean;
dark: {
backgroundColor: string | null;
foregroundColor: string;
uncommittedForegroundColor: string | null;
};
light: {
backgroundColor: string | null;
foregroundColor: string;
uncommittedForegroundColor: string | null;
};
};
hover: {
separateLines: boolean;
};
};
line: {
trailing: {
dark: {
backgroundColor: string | null;
foregroundColor: string;
};
light: {
backgroundColor: string | null;
foregroundColor: string;
};
};
};
};
lineHighlight: {
dark: {
backgroundColor: string;
overviewRulerColor: string;
};
light: {
backgroundColor: string;
overviewRulerColor: string;
};
};
}
export const themeDefaults: IThemeConfig = {
annotations: {
file: {
gutter: {
separateLines: true,
dark: {
backgroundColor: null,
foregroundColor: 'rgb(190, 190, 190)',
uncommittedForegroundColor: null
},
light: {
backgroundColor: null,
foregroundColor: 'rgb(116, 116, 116)',
uncommittedForegroundColor: null
}
},
hover: {
separateLines: false
}
},
line: {
trailing: {
dark: {
backgroundColor: null,
foregroundColor: 'rgba(153, 153, 153, 0.35)'
},
light: {
backgroundColor: null,
foregroundColor: 'rgba(153, 153, 153, 0.35)'
}
}
}
},
lineHighlight: {
dark: {
backgroundColor: 'rgba(0, 188, 242, 0.2)',
overviewRulerColor: 'rgba(0, 188, 242, 0.6)'
},
light: {
backgroundColor: 'rgba(0, 188, 242, 0.2)',
overviewRulerColor: 'rgba(0, 188, 242, 0.6)'
}
}
};
export interface IConfig {
annotations: {
file: {
gutter: {
format: string;
dateFormat: string;
compact: boolean;
heatmap: {
enabled: boolean;
location: 'left' | 'right';
};
hover: {
details: boolean;
wholeLine: boolean;
};
};
hover: {
heatmap: {
enabled: boolean;
};
wholeLine: boolean;
};
};
line: {
hover: {
details: boolean;
changes: boolean;
};
trailing: {
format: string;
dateFormat: string;
hover: {
changes: boolean;
details: boolean;
wholeLine: boolean;
};
};
};
};
blame: {
file: {
annotationType: FileAnnotationType;
lineHighlight: {
enabled: boolean;
locations: BlameLineHighlightLocations[];
};
};
line: {
enabled: boolean;
annotationType: LineAnnotationType;
};
};
codeLens: {
enabled: boolean;
recentChange: {
enabled: boolean;
command: CodeLensCommand;
};
authors: {
enabled: boolean;
command: CodeLensCommand;
};
locations: CodeLensLocations[];
customLocationSymbols: string[];
perLanguageLocations: ICodeLensLanguageLocation[];
debug: boolean;
};
statusBar: {
enabled: boolean;
alignment: 'left' | 'right';
command: StatusBarCommand;
format: string;
dateFormat: string;
};
theme: IThemeConfig;
debug: boolean;
outputLevel: OutputLevel;
blame: IBlameConfig;
codeLens: ICodeLensesConfig;
statusBar: IStatusBarConfig;
advanced: IAdvancedConfig;
insiders: boolean;
outputLevel: OutputLevel;
advanced: IAdvancedConfig;
}

View File

@@ -31,9 +31,10 @@ export const DocumentSchemes = {
GitLensGit: 'gitlens-git' as DocumentSchemes
};
export type WorkspaceState = 'repoPath' | 'suppressGitVersionWarning' | 'suppressUpdateNotice';
export type WorkspaceState = 'repoPath' | 'suppressGitVersionWarning' | 'suppressUpdateNotice' | 'suppressWelcomeNotice';
export const WorkspaceState = {
GitLensVersion: 'gitlensVersion' as WorkspaceState,
SuppressGitVersionWarning: 'suppressGitVersionWarning' as WorkspaceState,
SuppressUpdateNotice: 'suppressUpdateNotice' as WorkspaceState
SuppressUpdateNotice: 'suppressUpdateNotice' as WorkspaceState,
SuppressWelcomeNotice: 'suppressWelcomeNotice' as WorkspaceState
};

View File

@@ -0,0 +1,437 @@
'use strict';
import { Functions, Objects } from './system';
import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { AnnotationController } from './annotations/annotationController';
import { Annotations, endOfLineIndex } from './annotations/annotations';
import { Commands } from './commands';
import { TextEditorComparer } from './comparers';
import { FileAnnotationType, IConfig, LineAnnotationType, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions);
export class CurrentLineController extends Disposable {
private _activeEditorLineDisposable: Disposable | undefined;
private _blameable: boolean;
private _config: IConfig;
private _currentLine: number = -1;
private _disposable: Disposable;
private _editor: TextEditor | undefined;
private _statusBarItem: StatusBarItem | undefined;
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<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._editor && this._editor.setDecorations(annotationDecoration, []);
this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
this._disposable && this._disposable.dispose();
}
private _onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<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;
if (this._editor) {
this._editor.setDecorations(annotationDecoration, []);
}
}
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) {
changed = true;
if (cfg.statusBar.enabled) {
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left;
if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) {
this._statusBarItem.dispose();
this._statusBarItem = undefined;
}
this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0);
this._statusBarItem.command = cfg.statusBar.command;
}
else if (!cfg.statusBar.enabled && this._statusBarItem) {
this._statusBarItem.dispose();
this._statusBarItem = undefined;
}
}
this._config = cfg;
if (!changed) return;
const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled;
if (trackCurrentLine && !this._activeEditorLineDisposable) {
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this));
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
this._activeEditorLineDisposable = Disposable.from(...subscriptions);
}
else if (!trackCurrentLine && this._activeEditorLineDisposable) {
this._activeEditorLineDisposable.dispose();
this._activeEditorLineDisposable = undefined;
}
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private isEditorBlameable(editor: TextEditor | undefined): boolean {
if (editor === undefined || editor.document === undefined) return false;
if (!this.git.isTrackable(editor.document.uri)) return false;
if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false;
return this.git.isEditorBlameable(editor);
}
private async _onActiveTextEditorChanged(editor: TextEditor | undefined) {
this._currentLine = -1;
const previousEditor = this._editor;
previousEditor && previousEditor.setDecorations(annotationDecoration, []);
if (editor === undefined || !this.isEditorBlameable(editor)) {
this.clear(editor);
this._editor = undefined;
return;
}
this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty;
this._editor = editor;
this._uri = await GitUri.fromUri(editor.document.uri, this.git);
const maxLines = this._config.advanced.caching.maxLines;
// If caching is on and the file is small enough -- kick off a blame for the whole file
if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) {
this.git.getBlameForFile(this._uri);
}
this._updateBlameDebounced(editor.selection.active.line, editor);
}
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
this._blameable = e.blameable;
if (!e.blameable || !this._editor) {
this.clear(e.editor);
return;
}
// Make sure this is for the editor we are tracking
if (!TextEditorComparer.equals(this._editor, e.editor)) return;
this._updateBlameDebounced(this._editor.selection.active.line, this._editor);
}
private _onAnnotationsToggled() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onGitCacheChanged() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<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);
}
}
async clear(editor: TextEditor | undefined, previousEditor?: TextEditor) {
this._clearAnnotations(editor, previousEditor);
this._statusBarItem && this._statusBarItem.hide();
}
private async _clearAnnotations(editor: TextEditor | undefined, previousEditor?: TextEditor) {
editor && editor.setDecorations(annotationDecoration, []);
// I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay
if (editor !== undefined) {
await Functions.wait(1);
editor.setDecorations(annotationDecoration, []);
}
}
async show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) {
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
this._updateStatusBar(commit);
await this._updateAnnotations(commit, blameLine, editor);
}
async showAnnotations(editor: TextEditor, type: LineAnnotationType) {
if (editor === undefined) return;
const cfg = this._config.blame.line;
if (!cfg.enabled || cfg.annotationType !== type) {
cfg.enabled = true;
cfg.annotationType = type;
await this._clearAnnotations(editor);
await this._updateBlame(editor.selection.active.line, editor);
}
}
async toggleAnnotations(editor: TextEditor, type: LineAnnotationType) {
if (editor === undefined) return;
const cfg = this._config.blame.line;
cfg.enabled = !cfg.enabled;
cfg.annotationType = type;
await this._clearAnnotations(editor);
await this._updateBlame(editor.selection.active.line, editor);
}
private async _updateAnnotations(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) {
const cfg = this._config.blame.line;
if (!cfg.enabled) return;
const line = blameLine.line + this._uri.offset;
const decorationOptions: DecorationOptions[] = [];
let showChanges = false;
let showChangesStartIndex = 0;
let showChangesInStartingWhitespace = false;
let showDetails = false;
let showDetailsStartIndex = 0;
let showDetailsInStartingWhitespace = false;
switch (cfg.annotationType) {
case LineAnnotationType.Trailing: {
const cfgAnnotations = this._config.annotations.line.trailing;
showChanges = cfgAnnotations.hover.changes;
showDetails = cfgAnnotations.hover.details;
if (cfgAnnotations.hover.wholeLine) {
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetailsStartIndex = 0;
showDetailsInStartingWhitespace = false;
}
else {
showChangesStartIndex = endOfLineIndex;
showChangesInStartingWhitespace = true;
showDetailsStartIndex = endOfLineIndex;
showDetailsInStartingWhitespace = true;
}
const decoration = Annotations.trailing(commit, cfgAnnotations.format, cfgAnnotations.dateFormat, this._config.theme);
decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex));
decorationOptions.push(decoration);
break;
}
case LineAnnotationType.Hover: {
const cfgAnnotations = this._config.annotations.line.hover;
showChanges = cfgAnnotations.changes;
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetails = cfgAnnotations.details;
showDetailsStartIndex = 0;
showDetailsInStartingWhitespace = false;
break;
}
}
if (showDetails || showChanges) {
const annotationType = this.annotationController.getAnnotationType(editor);
const firstNonWhitespace = editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
switch (annotationType) {
case FileAnnotationType.Gutter: {
const cfgHover = this._config.annotations.file.gutter.hover;
if (cfgHover.details) {
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
}
else {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
}
}
break;
}
case FileAnnotationType.Hover: {
const cfgHover = this._config.annotations.file.hover;
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
showChangesStartIndex = 0;
}
else {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
}
break;
}
}
if (showDetails) {
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
if (!commit.isUncommitted) {
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha);
}
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
const decoration = Annotations.detailsHover(logCommit || commit);
decoration.range = editor.document.validateRange(new Range(line, showDetailsStartIndex, line, endOfLineIndex));
decorationOptions.push(decoration);
if (showDetailsInStartingWhitespace && showDetailsStartIndex !== 0) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace));
}
}
if (showChanges) {
const decoration = await Annotations.changesHover(commit, line, this._uri, this.git);
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
decoration.range = editor.document.validateRange(new Range(line, showChangesStartIndex, line, endOfLineIndex));
decorationOptions.push(decoration);
if (showChangesInStartingWhitespace && showChangesStartIndex !== 0) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace));
}
}
}
if (decorationOptions.length) {
editor.setDecorations(annotationDecoration, decorationOptions);
}
}
private _updateStatusBar(commit: GitCommit) {
const cfg = this._config.statusBar;
if (!cfg.enabled || this._statusBarItem === undefined) return;
this._statusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, cfg.dateFormat)}`;
switch (cfg.command) {
case StatusBarCommand.BlameAnnotate:
this._statusBarItem.tooltip = 'Toggle Blame Annotations';
break;
case StatusBarCommand.ShowBlameHistory:
this._statusBarItem.tooltip = 'Open Blame History Explorer';
break;
case StatusBarCommand.ShowFileHistory:
this._statusBarItem.tooltip = 'Open File History Explorer';
break;
case StatusBarCommand.DiffWithPrevious:
this._statusBarItem.command = Commands.DiffLineWithPrevious;
this._statusBarItem.tooltip = 'Compare Line Commit with Previous';
break;
case StatusBarCommand.DiffWithWorking:
this._statusBarItem.command = Commands.DiffLineWithWorking;
this._statusBarItem.tooltip = 'Compare Line Commit with Working Tree';
break;
case StatusBarCommand.ToggleCodeLens:
this._statusBarItem.tooltip = 'Toggle Git CodeLens';
break;
case StatusBarCommand.ShowQuickCommitDetails:
this._statusBarItem.tooltip = 'Show Commit Details';
break;
case StatusBarCommand.ShowQuickCommitFileDetails:
this._statusBarItem.tooltip = 'Show Line Commit Details';
break;
case StatusBarCommand.ShowQuickFileHistory:
this._statusBarItem.tooltip = 'Show File History';
break;
case StatusBarCommand.ShowQuickCurrentBranchHistory:
this._statusBarItem.tooltip = 'Show Branch History';
break;
}
this._statusBarItem.show();
}
}

View File

@@ -1,24 +1,24 @@
'use strict';
import { Objects } from './system';
import { commands, ExtensionContext, extensions, languages, Uri, window, workspace } from 'vscode';
import { BlameActiveLineController } from './blameActiveLineController';
import { BlameAnnotationController } from './blameAnnotationController';
import { AnnotationController } from './annotations/annotationController';
import { CommandContext, setCommandContext } from './commands';
import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands';
import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands';
import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands';
import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
import { ShowBlameCommand, ToggleBlameCommand } from './commands';
import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleLineBlameCommand } from './commands';
import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands';
import { ShowLastQuickPickCommand } from './commands';
import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands';
import { ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowCommitSearchCommand } from './commands';
import { ShowCommitSearchCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand } from './commands';
import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './commands';
import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands';
import { ToggleCodeLensCommand } from './commands';
import { Keyboard } from './commands';
import { IConfig } from './configuration';
import { ApplicationInsightsKey, BuiltInCommands, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants';
import { CurrentLineController } from './currentLineController';
import { GitContentProvider } from './gitContentProvider';
import { GitContextTracker, GitService } from './gitService';
import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
@@ -74,11 +74,11 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git)));
const annotationController = new BlameAnnotationController(context, git, gitContextTracker);
const annotationController = new AnnotationController(context, git, gitContextTracker);
context.subscriptions.push(annotationController);
const activeLineController = new BlameActiveLineController(context, git, gitContextTracker, annotationController);
context.subscriptions.push(activeLineController);
const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController);
context.subscriptions.push(currentLineController);
context.subscriptions.push(new Keyboard());
@@ -98,8 +98,10 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new OpenFileInRemoteCommand(git));
context.subscriptions.push(new OpenInRemoteCommand());
context.subscriptions.push(new OpenRepoInRemoteCommand(git));
context.subscriptions.push(new ShowBlameCommand(annotationController));
context.subscriptions.push(new ToggleBlameCommand(annotationController));
context.subscriptions.push(new ShowFileBlameCommand(annotationController));
context.subscriptions.push(new ShowLineBlameCommand(currentLineController));
context.subscriptions.push(new ToggleFileBlameCommand(annotationController));
context.subscriptions.push(new ToggleLineBlameCommand(currentLineController));
context.subscriptions.push(new ShowBlameHistoryCommand(git));
context.subscriptions.push(new ShowFileHistoryCommand(git));
context.subscriptions.push(new ShowLastQuickPickCommand());
@@ -127,6 +129,20 @@ async function notifyOnNewGitLensVersion(context: ExtensionContext, version: str
const previousVersion = context.globalState.get<string>(WorkspaceState.GitLensVersion);
if (!context.globalState.get(WorkspaceState.SuppressWelcomeNotice, false)) {
await context.globalState.update(WorkspaceState.SuppressWelcomeNotice, true);
if (previousVersion === undefined) {
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.`, 'View Docs');
if (result === 'View Docs') {
// TODO: Reset before release
// commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens'));
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://github.com/eamodio/vscode-gitlens/blob/develop/README.md'));
}
return;
}
}
await context.globalState.update(WorkspaceState.GitLensVersion, version);
if (previousVersion) {
@@ -137,7 +153,9 @@ async function notifyOnNewGitLensVersion(context: ExtensionContext, version: str
const result = await window.showInformationMessage(`GitLens has been updated to v${version}`, 'View Release Notes', `Don't Show Again`);
if (result === 'View Release Notes') {
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens/changelog'));
// TODO: Reset before release
// commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://marketplace.visualstudio.com/items/eamodio.gitlens/changelog'));
commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://github.com/eamodio/vscode-gitlens/blob/develop/CHANGELOG.md'));
}
else if (result === `Don't Show Again`) {
context.globalState.update(WorkspaceState.SuppressUpdateNotice, true);

View File

@@ -0,0 +1,160 @@
'use strict';
import { Strings } from '../../system';
import { GitCommit } from '../models/commit';
import { IGitDiffLine } from '../models/diff';
import * as moment from 'moment';
export interface ICommitFormatOptions {
dateFormat?: string | null;
tokenOptions?: {
ago?: Strings.ITokenOptions;
author?: Strings.ITokenOptions;
authorAgo?: Strings.ITokenOptions;
date?: Strings.ITokenOptions;
message?: Strings.ITokenOptions;
};
}
export class CommitFormatter {
private _options: ICommitFormatOptions;
constructor(private commit: GitCommit, options?: ICommitFormatOptions) {
options = options || {};
if (options.tokenOptions == null) {
options.tokenOptions = {};
}
if (options.dateFormat == null) {
options.dateFormat = 'MMMM Do, YYYY h:MMa';
}
this._options = options;
}
get ago() {
const ago = moment(this.commit.date).fromNow();
return this._padOrTruncate(ago, this._options.tokenOptions!.ago);
}
get author() {
const author = this.commit.author;
return this._padOrTruncate(author, this._options.tokenOptions!.author);
}
get authorAgo() {
const authorAgo = `${this.commit.author}, ${moment(this.commit.date).fromNow()}`;
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo);
}
get date() {
const date = moment(this.commit.date).format(this._options.dateFormat!);
return this._padOrTruncate(date, this._options.tokenOptions!.date);
}
get id() {
return this.commit.shortSha;
}
get message() {
const message = this.commit.isUncommitted ? 'Uncommitted change' : this.commit.message;
return this._padOrTruncate(message, this._options.tokenOptions!.message);
}
get sha() {
return this.id;
}
private collapsableWhitespace: number = 0;
private _padOrTruncate(s: string, options: Strings.ITokenOptions | undefined) {
// NOTE: the collapsable whitespace logic relies on the javascript template evaluation to be left to right
if (options === undefined) {
options = {
truncateTo: undefined,
padDirection: 'left',
collapseWhitespace: false
};
}
let max = options.truncateTo;
if (max === undefined) {
if (this.collapsableWhitespace === 0) return s;
// If we have left over whitespace make sure it gets re-added
const diff = this.collapsableWhitespace - s.length;
this.collapsableWhitespace = 0;
if (diff <= 0) return s;
if (options.truncateTo === undefined) return s;
return Strings.padLeft(s, diff);
}
max += this.collapsableWhitespace;
this.collapsableWhitespace = 0;
const diff = max - s.length;
if (diff > 0) {
if (options.collapseWhitespace) {
this.collapsableWhitespace = diff;
}
if (options.padDirection === 'left') return Strings.padLeft(s, max);
if (options.collapseWhitespace) {
max -= diff;
}
return Strings.padRight(s, max);
}
if (diff < 0) return Strings.truncate(s, max);
return s;
}
static fromTemplate(template: string, commit: GitCommit, dateFormat: string | null): string;
static fromTemplate(template: string, commit: GitCommit, options?: ICommitFormatOptions): string;
static fromTemplate(template: string, commit: GitCommit, dateFormatOrOptions?: string | null | ICommitFormatOptions): string;
static fromTemplate(template: string, commit: GitCommit, dateFormatOrOptions?: string | null | ICommitFormatOptions): string {
let options: ICommitFormatOptions | undefined = undefined;
if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') {
const tokenOptions = Strings.getTokensFromTemplate(template)
.reduce((map, token) => {
map[token.key] = token.options;
return map;
}, {} as { [token: string]: ICommitFormatOptions });
options = {
dateFormat: dateFormatOrOptions,
tokenOptions: tokenOptions
};
}
else {
options = dateFormatOrOptions;
}
return Strings.interpolateLazy(template, new CommitFormatter(commit, options));
}
static toHoverAnnotation(commit: GitCommit, dateFormat: string = 'MMMM Do, YYYY h:MMa'): string | string[] {
const message = commit.isUncommitted ? '' : `\n\n> ${commit.message.replace(/\n/g, '\n>\n> ')}`;
return `\`${commit.shortSha}\` &nbsp; __${commit.author}__, ${moment(commit.date).fromNow()} &nbsp; _(${moment(commit.date).format(dateFormat)})_${message}`;
}
static toHoverDiff(commit: GitCommit, previous: IGitDiffLine | undefined, current: IGitDiffLine | undefined): string | undefined {
if (previous === undefined && current === undefined) return undefined;
const codeDiff = this._getCodeDiff(previous, current);
return commit.isUncommitted
? `\`Changes\` &nbsp; \u2014 &nbsp; _uncommitted_\n${codeDiff}`
: `\`Changes\` &nbsp; \u2014 &nbsp; \`${commit.previousShortSha}\` \u2194 \`${commit.shortSha}\`\n${codeDiff}`;
}
private static _getCodeDiff(previous: IGitDiffLine | undefined, current: IGitDiffLine | undefined): string {
return `\`\`\`
- ${previous === undefined ? '' : previous.line.trim()}
+ ${current === undefined ? '' : current.line.trim()}
\`\`\``;
}
}

View File

@@ -5,6 +5,7 @@ import { spawnPromise } from 'spawn-rx';
import * as fs from 'fs';
import * as path from 'path';
import * as tmp from 'tmp';
import * as iconv from 'iconv-lite';
export { IGit };
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 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 {
// 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
args.splice(0, 0, '-c', 'core.quotepath=false');
const s = await spawnPromise(git.path, args, { cwd: cwd });
Logger.log('git', ...args, ` cwd='${cwd}'`);
return s;
const opts = { encoding: 'utf8', ...options };
const s = await spawnPromise(git.path, args, { cwd: options.cwd, encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' });
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) {
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'))) {
Logger.warn('git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
return '';
}
else {
Logger.error(ex, 'git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
if (msg) {
for (const warning of GitWarnings) {
if (warning.test(msg)) {
Logger.warn('git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
return '';
}
}
}
Logger.error(ex, 'git', ...args, ` cwd='${options.cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`);
throw ex;
}
}
@@ -62,14 +81,14 @@ export class Git {
static async getRepoPath(cwd: string | undefined) {
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 '';
return data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/');
}
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 ext = path.extname(fileName);
@@ -82,7 +101,7 @@ export class Git {
}
Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${branchOrSha}); destination=${destination}`);
fs.appendFile(destination, data, err => {
fs.appendFile(destination, data, { encoding: 'binary' }, err => {
if (err) {
reject(err);
return;
@@ -144,7 +163,7 @@ export class Git {
params.push(sha);
}
return gitCommand(root, ...params, `--`, file);
return gitCommand({ cwd: root }, ...params, `--`, file);
}
static branch(repoPath: string, all: boolean) {
@@ -153,19 +172,19 @@ export class Git {
params.push(`-a`);
}
return gitCommand(repoPath, ...params);
return gitCommand({ cwd: repoPath }, ...params);
}
static async config_get(key: string, repoPath?: string) {
try {
return await gitCommand(repoPath || '', `config`, `--get`, key);
return await gitCommand({ cwd: repoPath || '' }, `config`, `--get`, key);
}
catch (ex) {
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`];
if (sha1) {
params.push(sha1);
@@ -174,7 +193,7 @@ export class Git {
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) {
@@ -186,7 +205,7 @@ export class Git {
params.push(sha2);
}
return gitCommand(repoPath, ...params);
return gitCommand({ cwd: repoPath }, ...params);
}
static difftool_dirDiff(repoPath: string, sha1: string, sha2?: string) {
@@ -195,7 +214,7 @@ export class Git {
params.push(sha2);
}
return gitCommand(repoPath, ...params);
return gitCommand({ cwd: repoPath }, ...params);
}
static log(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false) {
@@ -213,7 +232,7 @@ export class Git {
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) {
@@ -250,7 +269,7 @@ export class Git {
params.push(`--`);
params.push(file);
return gitCommand(root, ...params);
return gitCommand({ cwd: root }, ...params);
}
static log_search(repoPath: string, search: string[] = [], maxCount?: number) {
@@ -259,12 +278,12 @@ export class Git {
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> {
try {
return await gitCommand(repoPath, 'ls-files', fileName);
return await gitCommand({ cwd: repoPath }, 'ls-files', fileName);
}
catch (ex) {
return '';
@@ -272,33 +291,33 @@ export class Git {
}
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> {
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);
branchOrSha = branchOrSha.replace('^', '');
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) {
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) {
if (!stashName) return undefined;
return gitCommand(repoPath, 'stash', 'drop', stashName);
return gitCommand({ cwd: repoPath }, 'stash', 'drop', stashName);
}
static stash_list(repoPath: string) {
return gitCommand(repoPath, ...defaultStashParams);
return gitCommand({ cwd: repoPath }, ...defaultStashParams);
}
static stash_save(repoPath: string, message?: string, unstagedOnly: boolean = false) {
@@ -309,18 +328,18 @@ export class Git {
if (message) {
params.push(message);
}
return gitCommand(repoPath, ...params);
return gitCommand({ cwd: repoPath }, ...params);
}
static status(repoPath: string, porcelainVersion: number = 1): Promise<string> {
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> {
const [file, root] = Git.splitPath(fileName, repoPath);
const porcelain = porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain';
return gitCommand(root, 'status', porcelain, file);
return gitCommand({ cwd: root }, 'status', porcelain, file);
}
}

View File

@@ -62,8 +62,8 @@ export class GitContextTracker extends Disposable {
}
private _onBlameFailed(key: string) {
const fileName = this._editor && this._editor.document && this._editor.document.fileName;
if (!fileName || key !== this.git.getCacheEntryKey(fileName)) return;
if (this._editor === undefined || this._editor.document === undefined || this._editor.document.uri === undefined) return;
if (key !== this.git.getCacheEntryKey(this._editor.document.uri)) return;
this._updateBlameability(false);
}
@@ -72,8 +72,8 @@ export class GitContextTracker extends Disposable {
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return;
// Can't unsubscribe here because undo doesn't trigger any other event
//this._unsubscribeToDocumentChanges();
//this.updateBlameability(false);
// this._unsubscribeToDocumentChanges();
// this.updateBlameability(false);
// We have to defer because isDirty is not reliable inside this event
setTimeout(() => this._updateBlameability(!e.document.isDirty), 1);
@@ -83,7 +83,7 @@ export class GitContextTracker extends Disposable {
if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return;
// Don't need to resubscribe as we aren't unsubscribing on document changes anymore
//this._subscribeToDocumentChanges();
// this._subscribeToDocumentChanges();
this._updateBlameability(!e.isDirty);
}

View File

@@ -83,15 +83,15 @@ export class GitUri extends Uri {
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 (uri.scheme === DocumentSchemes.Git && uri.query === '~') {
if (uri.scheme === DocumentSchemes.Git) {
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);
}

View File

@@ -1,18 +1,24 @@
'use strict';
export interface IGitDiffLine {
line: string;
state: 'added' | 'removed' | 'unchanged';
}
export interface IGitDiffChunk {
current: (IGitDiffLine | undefined)[];
currentStart: number;
currentEnd: number;
previous: (IGitDiffLine | undefined)[];
previousStart: number;
previousEnd: number;
chunk?: string;
original: (string | undefined)[];
originalStart: number;
originalEnd: number;
changes: (string | undefined)[];
changesStart: number;
changesEnd: number;
}
export interface IGitDiff {
diff?: string;
chunks: IGitDiffChunk[];
diff?: string;
}

View File

@@ -1,7 +1,7 @@
'use strict';
import { Uri } from 'vscode';
import { GitCommit, GitCommitType, IGitCommitLine } from './commit';
import { IGitStatusFile, GitStatusFileStatus } from './status';
import { GitStatusFileStatus, IGitStatusFile } from './status';
import * as path from 'path';
export class GitLogCommit extends GitCommit {

View File

@@ -1,7 +1,7 @@
'use strict';
import { IGitCommitLine } from './commit';
import { GitLogCommit } from './logCommit';
import { IGitStatusFile, GitStatusFileStatus } from './status';
import { GitStatusFileStatus, IGitStatusFile } from './status';
export class GitStashCommit extends GitLogCommit {

View File

@@ -41,7 +41,7 @@ export class GitBlameParser {
let entry: IBlameEntry | undefined = undefined;
let position = -1;
while (++position < lines.length) {
let lineParts = lines[position].split(' ');
const lineParts = lines[position].split(' ');
if (lineParts.length < 2) {
continue;
}
@@ -60,7 +60,7 @@ export class GitBlameParser {
switch (lineParts[0]) {
case 'author':
entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted'
? 'You'
: lineParts.slice(1).join(' ').trim();
break;
@@ -122,7 +122,7 @@ export class GitBlameParser {
const authors: Map<string, IGitAuthor> = new Map();
const commits: Map<string, GitCommit> = new Map();
const lines: Array<IGitCommitLine> = [];
const lines: IGitCommitLine[] = [];
let relativeFileName = repoPath && fileName;

View File

@@ -1,5 +1,5 @@
'use strict';
import { IGitDiff, IGitDiffChunk } from './../git';
import { IGitDiff, IGitDiffChunk, IGitDiffLine } from './../git';
const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm;
@@ -15,22 +15,47 @@ export class GitDiffParser {
match = unifiedDiffRegex.exec(`${data}\n@@`);
if (match == null) break;
const originalStart = +match[1];
const changedStart = +match[3];
const previousStart = +match[1];
const currentStart = +match[3];
const chunk = match[5];
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);
const current: (IGitDiffLine | undefined)[] = [];
const previous: (IGitDiffLine | 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;
}
}
chunks.push({
chunk: debug ? chunk : undefined,
original: original,
originalStart: originalStart,
originalEnd: originalStart + +match[2],
changes: changed,
changesStart: changedStart,
changesEnd: changedStart + +match[4]
current: current,
currentStart: currentStart,
currentEnd: currentStart + +match[4],
previous: previous,
previousStart: previousStart,
previousEnd: previousStart + +match[2]
});
} while (match != null);

View File

@@ -1,6 +1,6 @@
'use strict';
import { Range } from 'vscode';
import { Git, GitStatusFileStatus, GitLogCommit, GitCommitType, IGitAuthor, IGitLog, IGitStatusFile } from './../git';
import { Git, GitCommitType, GitLogCommit, GitStatusFileStatus, IGitAuthor, IGitLog, IGitStatusFile } from './../git';
// import { Logger } from '../../logger';
import * as moment from 'moment';
import * as path from 'path';
@@ -61,7 +61,7 @@ export class GitLogParser {
switch (lineParts[0]) {
case 'author':
entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted'
? 'You'
: lineParts.slice(1).join(' ').trim();
break;

View File

@@ -1,5 +1,5 @@
'use strict';
import { Git, GitStatusFileStatus, GitStatusFile, IGitStatus } from './../git';
import { Git, GitStatusFile, GitStatusFileStatus, IGitStatus } from './../git';
interface IFileStatusEntry {
staged: boolean;
@@ -61,7 +61,7 @@ export class GitStatusParser {
else {
let entry: IFileStatusEntry;
const rawStatus = line.substring(0, 2);
let fileName = line.substring(3);
const fileName = line.substring(3);
if (rawStatus[0] === 'R') {
const [file1, file2] = fileName.replace(/\"/g, '').split('->');
entry = this._parseFileEntry(rawStatus, file2.trim(), file1.trim());
@@ -98,7 +98,7 @@ export class GitStatusParser {
}
}
else {
let lineParts = line.split(' ');
const lineParts = line.split(' ');
let entry: IFileStatusEntry | undefined = undefined;
switch (lineParts[0][0]) {
case '1': // normal

View File

@@ -21,7 +21,7 @@ export class BitbucketService extends RemoteProvider {
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line: string = '';
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `#${fileName}-${range.start.line}`;

View File

@@ -21,7 +21,7 @@ export class GitHubService extends RemoteProvider {
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line: string = '';
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `#L${range.start.line}`;

View File

@@ -21,7 +21,7 @@ export class VisualStudioService extends RemoteProvider {
}
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
let line: string = '';
let line = '';
if (range) {
if (range.start.line === range.end.line) {
line = `&line=${range.start.line}`;

View File

@@ -3,7 +3,7 @@ import { Functions, Iterables, Strings } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands';
import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants';
import { CodeLensCommand, CodeLensLocation, IConfig, ICodeLensLanguageLocation } from './configuration';
import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration';
import { GitCommit, GitService, GitUri, IGitBlame, IGitBlameLines } from './gitService';
import { Logger } from './logger';
import * as moment from 'moment';
@@ -56,24 +56,22 @@ export class GitCodeLensProvider implements CodeLensProvider {
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
this._documentIsDirty = document.isDirty;
let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId);
let languageLocations = this._config.codeLens.perLanguageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId);
if (languageLocations == null) {
languageLocations = {
language: undefined,
location: this._config.codeLens.location,
customSymbols: this._config.codeLens.locationCustomSymbols
locations: this._config.codeLens.locations,
customSymbols: this._config.codeLens.customLocationSymbols
} as ICodeLensLanguageLocation;
}
const lenses: CodeLens[] = [];
if (languageLocations.location === CodeLensLocation.None) return lenses;
const gitUri = await GitUri.fromUri(document.uri, this.git);
const blamePromise = this.git.getBlameForFile(gitUri);
let blame: IGitBlame | undefined;
if (languageLocations.location === CodeLensLocation.Document) {
if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) {
blame = await blamePromise;
if (blame === undefined || !blame.lines.length) return lenses;
}
@@ -91,7 +89,8 @@ export class GitCodeLensProvider implements CodeLensProvider {
symbols.forEach(sym => this._provideCodeLens(gitUri, document, sym, languageLocations!, blame!, lenses));
}
if (languageLocations.location !== CodeLensLocation.Custom || (languageLocations.customSymbols || []).find(_ => _.toLowerCase() === 'file')) {
if (languageLocations.locations.includes(CodeLensLocations.Document) ||
(languageLocations.locations.includes(CodeLensLocations.Custom) && (languageLocations.customSymbols || []).find(_ => _.toLowerCase() === 'file'))) {
// Check if we have a lens for the whole document -- if not add one
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
@@ -115,57 +114,63 @@ export class GitCodeLensProvider implements CodeLensProvider {
}
private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined {
let valid: boolean = false;
let valid = false;
let range: Range | undefined;
switch (languageLocation.location) {
case CodeLensLocation.All:
case CodeLensLocation.DocumentAndContainers:
switch (symbol.kind) {
case SymbolKind.File:
valid = true;
// Adjust the range to be the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
break;
case SymbolKind.Package:
case SymbolKind.Module:
// Adjust the range to be the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
valid = true;
break;
case SymbolKind.Namespace:
case SymbolKind.Class:
case SymbolKind.Interface:
valid = true;
break;
case SymbolKind.Constructor:
case SymbolKind.Method:
case SymbolKind.Function:
case SymbolKind.Property:
case SymbolKind.Enum:
valid = languageLocation.location === CodeLensLocation.All;
break;
switch (symbol.kind) {
case SymbolKind.File:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
else if (languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
if (valid) {
// Adjust the range to be for the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
break;
case CodeLensLocation.Custom:
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
case SymbolKind.Package:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
else if (languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
if (valid) {
switch (symbol.kind) {
case SymbolKind.File:
// Adjust the range to be the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
break;
case SymbolKind.Package:
case SymbolKind.Module:
// Adjust the range to be the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
break;
// Adjust the range to be for the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
}
break;
case SymbolKind.Class:
case SymbolKind.Interface:
case SymbolKind.Module:
case SymbolKind.Namespace:
case SymbolKind.Struct:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
break;
case SymbolKind.Constructor:
case SymbolKind.Enum:
case SymbolKind.Function:
case SymbolKind.Method:
case SymbolKind.Property:
if (languageLocation.locations.includes(CodeLensLocations.Blocks)) {
valid = true;
}
break;
}
if (!valid && languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
return valid ? range || symbol.location.range : undefined;
@@ -302,7 +307,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
_applyBlameAnnotateCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines): T {
lens.command = {
title: title,
command: Commands.ToggleBlame,
command: Commands.ToggleFileBlame,
arguments: [Uri.file(lens.uri.fsPath)]
};
return lens;
@@ -360,7 +365,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
}
_applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T {
if (!commit) {
if (commit === undefined) {
const blameLine = blame.allLines[lens.range.start.line];
commit = blame.commits.get(blameLine.sha);
}
@@ -382,7 +387,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
_applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T {
lens.command = {
title: title,
command: CodeLensCommand.ShowQuickCommitDetails,
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails,
arguments: [
Uri.file(lens.uri.fsPath),
{
@@ -396,7 +401,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
_applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit): T {
lens.command = {
title: title,
command: CodeLensCommand.ShowQuickCommitFileDetails,
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails,
arguments: [
Uri.file(lens.uri.fsPath),
{

View File

@@ -1,10 +1,10 @@
'use strict';
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 { CodeLensVisibility, IConfig } from './configuration';
import { IConfig } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitLog, IGitStash, IGitStatus } from './git/git';
import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitDiffLine, IGitLog, IGitStash, IGitStatus, setDefaultEncoding } from './git/git';
import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { GitCodeLensProvider } from './gitCodeLensProvider';
import { Logger } from './logger';
@@ -15,6 +15,7 @@ import * as path from 'path';
export { GitUri, IGitCommitInfo };
export * from './git/models/models';
export * from './git/formatters/commit';
export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider';
export * from './git/gitContextTracker';
@@ -33,17 +34,16 @@ class GitCacheEntry {
return Iterables.every(this.cache.values(), _ => _.errorMessage !== undefined);
}
get<T extends ICachedBlame | ICachedDiff | ICachedLog > (key: string): T | undefined {
get<T extends ICachedBlame | ICachedDiff | ICachedLog>(key: string): T | undefined {
return this.cache.get(key) as T;
}
set<T extends ICachedBlame | ICachedDiff | ICachedLog > (key: string, value: T) {
set<T extends ICachedBlame | ICachedDiff | ICachedLog>(key: string, value: T) {
this.cache.set(key, value);
}
}
interface ICachedItem<T> {
//date: Date;
item: Promise<T>;
errorMessage?: string;
}
@@ -130,6 +130,9 @@ export class GitService extends Disposable {
}
private _onConfigurationChanged() {
const encoding = workspace.getConfiguration('files').get<string>('encoding', 'utf8');
setDefaultEncoding(encoding);
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
const codeLensChanged = !Objects.areEquivalent(cfg.codeLens, this.config && this.config.codeLens);
@@ -137,7 +140,7 @@ export class GitService extends Disposable {
if (codeLensChanged) {
Logger.log('CodeLens config changed; resetting CodeLens provider');
if (cfg.codeLens.visibility === CodeLensVisibility.Auto && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (this._codeLensProvider) {
this._codeLensProvider.reset();
}
@@ -152,7 +155,7 @@ export class GitService extends Disposable {
this._codeLensProvider = undefined;
}
setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.visibility !== CodeLensVisibility.Off && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled));
setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled);
}
if (advancedChanged) {
@@ -164,6 +167,7 @@ export class GitService extends Disposable {
const disposables: Disposable[] = [];
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(this._fsWatcher.onDidChange(this._onGitChanged, this));
@@ -206,6 +210,21 @@ export class GitService extends Disposable {
this.config = cfg;
}
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
if (!this.UseCaching) return;
if (e.document.uri.scheme !== DocumentSchemes.File) return;
// 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() {
this._gitCache.clear();
@@ -217,7 +236,7 @@ export class GitService extends Disposable {
if (!this.UseCaching) return;
if (document.uri.scheme !== DocumentSchemes.File) return;
const cacheKey = this.getCacheEntryKey(document.fileName);
const cacheKey = this.getCacheEntryKey(document.uri);
if (reason === RemoveCacheReason.DocumentSaved) {
// Don't remove broken blame on save (since otherwise we'll have to run the broken blame again)
@@ -238,7 +257,7 @@ export class GitService extends Disposable {
}
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> {
@@ -311,7 +330,7 @@ export class GitService extends Disposable {
public async getBlameability(uri: GitUri): Promise<boolean> {
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);
if (entry === undefined) return await this.isTracked(uri);
@@ -319,16 +338,14 @@ export class GitService extends Disposable {
}
async getBlameForFile(uri: GitUri): Promise<IGitBlame | undefined> {
let key: string = 'blame';
let key = 'blame';
if (uri.sha !== undefined) {
key += `:${uri.sha}`;
}
const fileName = uri.fsPath;
let entry: GitCacheEntry | undefined;
if (this.UseCaching) {
const cacheKey = this.getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(uri);
entry = this._gitCache.get(cacheKey);
if (entry !== undefined) {
@@ -337,19 +354,6 @@ export class GitService extends Disposable {
Logger.log(`Cached(${key}): getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
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})`);
@@ -363,13 +367,12 @@ export class GitService extends Disposable {
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) {
Logger.log(`Add blame cache for '${entry.key}:${key}'`);
entry.set<ICachedBlame>(key, {
//date: new Date(),
item: promise
} as ICachedBlame);
}
@@ -377,12 +380,12 @@ export class GitService extends Disposable {
return promise;
}
private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined, key: string): Promise<IGitBlame | undefined> {
const [file, root] = Git.splitPath(fileName, uri.repoPath, false);
private async _getBlameForFile(uri: GitUri, entry: GitCacheEntry | undefined, key: string): Promise<IGitBlame | undefined> {
const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
const ignore = await this._gitignore;
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) {
this._onDidBlameFail.fire(entry.key);
}
@@ -400,7 +403,6 @@ export class GitService extends Disposable {
Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedBlame>(key, {
//date: new Date(),
item: GitService.EmptyPromise,
errorMessage: msg
} as ICachedBlame);
@@ -519,7 +521,7 @@ export class GitService extends Disposable {
const commitCount = blame.commits.size;
const locations: Array<Location> = [];
const locations: Location[] = [];
Iterables.forEach(blame.commits.values(), (c, i) => {
if (c.isUncommitted) return;
@@ -550,8 +552,10 @@ export class GitService extends Disposable {
return branches;
}
getCacheEntryKey(fileName: string) {
return Git.normalizePath(fileName).toLowerCase();
getCacheEntryKey(fileName: string): string;
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> {
@@ -560,14 +564,18 @@ export class GitService extends Disposable {
return await Git.config_get(key, repoPath);
}
getGitUriForFile(fileName: string) {
const cacheKey = this.getCacheEntryKey(fileName);
getGitUriForFile(uri: Uri) {
const cacheKey = this.getCacheEntryKey(uri);
const entry = this._uriCache.get(cacheKey);
return entry && entry.uri;
}
async getDiffForFile(repoPath: string | undefined, fileName: string, sha1?: string, sha2?: string): Promise<IGitDiff | undefined> {
let key: string = 'diff';
async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise<IGitDiff | undefined> {
if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) {
sha2 = uri.sha;
}
let key = 'diff';
if (sha1 !== undefined) {
key += `:${sha1}`;
}
@@ -577,18 +585,18 @@ export class GitService extends Disposable {
let entry: GitCacheEntry | undefined;
if (this.UseCaching) {
const cacheKey = this.getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(uri);
entry = this._gitCache.get(cacheKey);
if (entry !== undefined) {
const cachedDiff = entry.get<ICachedDiff>(key);
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;
}
}
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) {
entry = new GitCacheEntry(cacheKey);
@@ -596,16 +604,15 @@ export class GitService extends Disposable {
}
}
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) {
Logger.log(`Add log cache for '${entry.key}:${key}'`);
entry.set<ICachedDiff>(key, {
//date: new Date(),
item: promise
} as ICachedDiff);
}
@@ -618,7 +625,7 @@ export class GitService extends Disposable {
try {
const data = await Git.diff(root, file, sha1, sha2);
return GitDiffParser.parse(data, this.config.debug);
return GitDiffParser.parse(data);
}
catch (ex) {
// Trap and cache expected diff errors
@@ -627,7 +634,6 @@ export class GitService extends Disposable {
Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedDiff>(key, {
//date: new Date(),
item: GitService.EmptyPromise,
errorMessage: msg
} as ICachedDiff);
@@ -639,21 +645,38 @@ export class GitService extends Disposable {
}
}
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<[IGitDiffLine | undefined, IGitDiffLine | undefined]> {
try {
const diff = await this.getDiffForFile(repoPath, fileName, sha1, sha2);
if (diff === undefined) return undefined;
const diff = await this.getDiffForFile(uri, sha1, sha2);
if (diff === undefined) return [undefined, undefined];
const chunk = diff.chunks.find(_ => Math.min(_.originalStart, _.changesStart) <= line && Math.max(_.originalEnd, _.changesEnd) >= line);
if (chunk === undefined) return undefined;
const chunk = diff.chunks.find(_ => _.currentStart <= line && _.currentEnd >= line);
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.currentStart + 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 [
chunk.original[line - chunk.originalStart + 1],
chunk.changes[line - chunk.changesStart + 1]
chunk.previous[line + deleted - 1],
chunk.current[line + deleted + (chunk.currentStart - chunk.previousStart)]
];
}
catch (ex) {
return undefined;
return [undefined, undefined];
}
}
@@ -729,7 +752,7 @@ export class GitService extends Disposable {
}
async getLogForFile(repoPath: string | undefined, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise<IGitLog | undefined> {
let key: string = 'log';
let key = 'log';
if (sha !== undefined) {
key += `:${sha}`;
}
@@ -785,7 +808,6 @@ export class GitService extends Disposable {
Logger.log(`Add log cache for '${entry.key}:${key}'`);
entry.set<ICachedLog>(key, {
//date: new Date(),
item: promise
} as ICachedLog);
}
@@ -813,7 +835,6 @@ export class GitService extends Disposable {
Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`);
entry.set<ICachedLog>(key, {
//date: new Date(),
item: GitService.EmptyPromise,
errorMessage: msg
} as ICachedLog);
@@ -833,7 +854,7 @@ export class GitService extends Disposable {
const commitCount = log.commits.size;
const locations: Array<Location> = [];
const locations: Location[] = [];
Iterables.forEach(log.commits.values(), (c, i) => {
if (c.isUncommitted) return;
@@ -849,7 +870,6 @@ export class GitService extends Disposable {
}
async getRemotes(repoPath: string): Promise<GitRemote[]> {
if (!this.config.insiders) return [];
if (!repoPath) return [];
Logger.log(`getRemotes('${repoPath}')`);
@@ -931,19 +951,10 @@ export class GitService extends Disposable {
return Git.show(repoPath, fileName, sha);
}
hasGitUriForFile(editor: TextEditor): boolean;
hasGitUriForFile(fileName: string): boolean;
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;
}
hasGitUriForFile(editor: TextEditor): boolean {
if (editor === undefined || editor.document === undefined || editor.document.uri === undefined) return false;
const cacheKey = this.getCacheEntryKey(fileName);
const cacheKey = this.getCacheEntryKey(editor.document.uri);
return this._uriCache.has(cacheKey);
}
@@ -998,8 +1009,7 @@ export class GitService extends Disposable {
}
toggleCodeLens(editor: TextEditor) {
if (this.config.codeLens.visibility === CodeLensVisibility.Off ||
(!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return;
if (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled) return;
Logger.log(`toggleCodeLens()`);
if (this._codeLensProviderDisposable) {

View File

@@ -83,7 +83,7 @@ export class OpenCommitFilesCommandQuickPickItem extends OpenFilesCommandQuickPi
super(uris, item || {
label: `$(file-symlink-file) Open Changed Files`,
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,7 +96,7 @@ export class OpenCommitWorkingTreeFilesCommandQuickPickItem extends OpenFilesCom
super(uris, item || {
label: `$(file-symlink-file) Open Changed Working Files`,
description: ''
//detail: `Opens all of the changed file in the working tree`
// detail: `Opens all of the changed file in the working tree`
});
}
}
@@ -110,7 +110,7 @@ export class CommitDetailsQuickPick {
let index = 0;
if (stash && git.config.insiders) {
if (stash) {
items.splice(index++, 0, new CommandQuickPickItem({
label: `$(git-pull-request) Apply Stashed Changes`,
description: `\u00a0 \u2014 \u00a0\u00a0 ${commit.message}`

View File

@@ -72,7 +72,7 @@ export class CommitFileDetailsQuickPick {
if (commit.previousSha) {
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}`
}, Commands.DiffWithPrevious, [
commit.uri,
@@ -85,7 +85,7 @@ export class CommitFileDetailsQuickPick {
if (commit.workingFileName) {
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}`
}, Commands.DiffWithWorking, [
Uri.file(path.resolve(commit.repoPath, commit.workingFileName)),

View File

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

View File

@@ -30,7 +30,7 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem {
constructor(remotes: GitRemote[], resource: RemoteResource, goBackCommand?: CommandQuickPickItem) {
const name = getNameFromRemoteResource(resource);
let description: string = '';
let description = '';
switch (resource.type) {
case 'branch':
description = `$(git-branch) ${resource.branch}`;

View File

@@ -16,7 +16,7 @@ export class OpenStatusFileCommandQuickPickItem extends OpenFileCommandQuickPick
directory = '';
}
let description = (status.status === 'R' && status.originalFileName)
const description = (status.status === 'R' && status.originalFileName)
? `${directory} \u00a0\u2190\u00a0 ${status.originalFileName}`
: directory;
@@ -46,7 +46,7 @@ export class OpenStatusFilesCommandQuickPickItem extends CommandQuickPickItem {
super(item || {
label: `$(file-symlink-file) Open Changed Files`,
description: ''
//detail: `Opens all of the changed files in the repository`
// detail: `Opens all of the changed files in the repository`
}, Commands.OpenChangedFiles, [
undefined,
{
@@ -211,7 +211,6 @@ export class RepoStatusQuickPick {
]));
}
if (goBackCommand) {
items.splice(0, 0, goBackCommand);
}

View File

@@ -10,7 +10,7 @@ export class StashListQuickPick {
static async show(git: GitService, stash: IGitStash, mode: 'list' | 'apply', goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem): Promise<CommitQuickPickItem | CommandQuickPickItem | undefined> {
const items = ((stash && Array.from(Iterables.map(stash.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[];
if (mode === 'list' && git.config.insiders) {
if (mode === 'list') {
items.splice(0, 0, new CommandQuickPickItem({
label: `$(repo-push) Stash Unstaged Changes`,
description: `\u00a0 \u2014 \u00a0\u00a0 stashes only unstaged changes`

View File

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

View File

@@ -55,7 +55,7 @@ export namespace Iterables {
}
export function join(source: Iterable<any>, separator: string): string {
let value: string = '';
let value = '';
const iterator = source[Symbol.iterator]();
let next = iterator.next();

View File

@@ -1,5 +1,4 @@
'use strict';
//import { isEqual as _isEqual } from 'lodash';
const _isEqual = require('lodash.isequal');
export namespace Objects {
@@ -8,13 +7,13 @@ export namespace Objects {
}
export function* entries(o: any): IterableIterator<[string, any]> {
for (let key in o) {
for (const key in o) {
yield [key, o[key]];
}
}
export function flatten(o: any, prefix: string = '', stringify: boolean = false): { [key: string]: any } {
let flattened = Object.create(null);
const flattened = Object.create(null);
_flatten(flattened, prefix, o, stringify);
return flattened;
}
@@ -37,7 +36,7 @@ export namespace Objects {
}
}
else if (Array.isArray(value)) {
let len = value.length;
const len = value.length;
for (let i = 0; i < len; i++) {
_flatten(flattened, `${key}[${i}]`, value[i], stringify);
}
@@ -47,7 +46,7 @@ export namespace Objects {
}
else {
let isEmpty = true;
for (let p in value) {
for (const p in value) {
isEmpty = false;
_flatten(flattened, key ? `${key}.${p}` : p, value[p], stringify);
}
@@ -56,4 +55,10 @@ export namespace Objects {
}
}
}
export function* values(o: any): IterableIterator<[any]> {
for (const key in o) {
yield [o[key]];
}
}
}

View File

@@ -1,9 +1,84 @@
'use strict';
//import { escapeRegExp as _escapeRegExp } from 'lodash';
import { Objects } from './object';
const _escapeRegExp = require('lodash.escaperegexp');
export namespace Strings {
export function escapeRegExp(s: string): string {
return _escapeRegExp(s);
}
const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g;
const TokenSanitizeRegex = /\$\{(\w*?)(?:\W|\d)*?\}/g;
export interface ITokenOptions {
padDirection: 'left' | 'right';
truncateTo: number | undefined;
collapseWhitespace: boolean;
}
export function getTokensFromTemplate(template: string) {
const tokens: { key: string, options: ITokenOptions }[] = [];
let match = TokenRegex.exec(template);
while (match != null) {
const truncateTo = match[2];
const option = match[3];
tokens.push({
key: match[1],
options: {
truncateTo: truncateTo == null ? undefined : parseInt(truncateTo, 10),
padDirection: option === '-' ? 'left' : 'right',
collapseWhitespace: option === '?'
}
});
match = TokenRegex.exec(template);
}
return tokens;
}
export function interpolate(template: string, tokens: { [key: string]: any }): string {
return new Function(...Object.keys(tokens), `return \`${template}\`;`)(...Objects.values(tokens));
}
export function interpolateLazy(template: string, context: object): string {
template = template.replace(TokenSanitizeRegex, '$${c.$1}');
return new Function('c', `return \`${template}\`;`)(context);
}
export function padLeft(s: string, padTo: number, padding: string = '\u00a0') {
const diff = padTo - s.length;
return (diff <= 0) ? s : '\u00a0'.repeat(diff) + s;
}
export function padLeftOrTruncate(s: string, max: number, padding?: string) {
if (s.length < max) return padLeft(s, max, padding);
if (s.length > max) return truncate(s, max);
return s;
}
export function padRight(s: string, padTo: number, padding: string = '\u00a0') {
const diff = padTo - s.length;
return (diff <= 0) ? s : s + '\u00a0'.repeat(diff);
}
export function padOrTruncate(s: string, max: number, padding?: string) {
const left = max < 0;
max = Math.abs(max);
if (s.length < max) return left ? padLeft(s, max, padding) : padRight(s, max, padding);
if (s.length > max) return truncate(s, max);
return s;
}
export function padRightOrTruncate(s: string, max: number, padding?: string) {
if (s.length < max) return padRight(s, max, padding);
if (s.length > max) return truncate(s, max);
return s;
}
export function truncate(s: string, truncateTo?: number) {
if (!s || truncateTo === undefined || s.length <= truncateTo) return s;
return `${s.substring(0, truncateTo - 1)}\u2026`;
}
}

View File

@@ -1,5 +1,5 @@
'use strict';
import { env, Disposable, version, workspace } from 'vscode';
import { Disposable, env, version, workspace } from 'vscode';
import * as os from 'os';
let _reporter: TelemetryReporter;

View File

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

View File

@@ -1,22 +1,52 @@
{
"rules": {
"arrow-parens": false,
"adjacent-overload-signatures": true,
"array-type": [
true,
"array"
],
"arrow-parens": [
true,
"ban-single-arg-parens"
],
"arrow-return-shorthand": true,
"class-name": true,
"comment-format": [
false
true,
"check-space"
],
"curly": [
true,
"ignore-same-line"
],
"curly": false,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"new-parens": true,
"no-duplicate-variable": true,
"no-angle-bracket-type-assertion": true,
"no-consecutive-blank-lines": [
true,
1
],
"no-default-export": true,
"no-eval": true,
"no-for-in-array": false,
// "no-for-in-array": true,
"no-inferrable-types": [
true,
"ignore-params",
"ignore-properties"
],
"no-internal-module": true,
// "no-invalid-template-strings": true,
"no-irregular-whitespace": true,
"no-reference": true,
"no-string-throw": true,
"no-trailing-whitespace": true,
"no-unnecessary-callback-wrapper": true,
// "no-unnecessary-qualifier": true,
// "no-unsafe-any": true,
"no-unsafe-finally": true,
"no-unused-expression": false,
"no-var-keyword": true,
@@ -35,11 +65,18 @@
"ignore-for-loop"
],
"ordered-imports": [
false,
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive"
}
],
"prefer-const": true,
"prefer-for-of": true,
"prefer-template": [
true,
"allow-single-concat"
],
"quotemark": [
true,
"single",
@@ -50,6 +87,16 @@
true,
"always"
],
"space-before-function-paren": [
true,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
// "strict-boolean-expressions": true,
// "strict-type-predicates": true,
"trailing-comma": [
true,
{
@@ -71,6 +118,7 @@
"variable-declaration": "nospace"
}
],
"typeof-compare": true,
"use-isnan": true,
"variable-name": [
true,