Переглянути джерело

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`)
main
Eric Amodio 7 роки тому
джерело
коміт
37e48ded2d
41 змінених файлів з 3539 додано та 2429 видалено
  1. +128
    -48
      README.md
  2. +1
    -1
      images/blame-dark.svg
  3. +1
    -1
      images/blame-light.svg
  4. +1263
    -929
      package.json
  5. +282
    -0
      src/annotations/annotationController.ts
  6. +74
    -0
      src/annotations/annotationProvider.ts
  7. +189
    -0
      src/annotations/annotations.ts
  8. +82
    -0
      src/annotations/blameAnnotationProvider.ts
  9. +69
    -0
      src/annotations/diffAnnotationProvider.ts
  10. +76
    -0
      src/annotations/gutterBlameAnnotationProvider.ts
  11. +49
    -0
      src/annotations/hoverBlameAnnotationProvider.ts
  12. +1
    -3
      src/annotations/whitespaceController.ts
  13. +0
    -391
      src/blameActiveLineController.ts
  14. +0
    -271
      src/blameAnnotationController.ts
  15. +0
    -113
      src/blameAnnotationFormatter.ts
  16. +0
    -302
      src/blameAnnotationProvider.ts
  17. +5
    -3
      src/commands.ts
  18. +7
    -5
      src/commands/common.ts
  19. +0
    -28
      src/commands/showBlame.ts
  20. +35
    -0
      src/commands/showFileBlame.ts
  21. +34
    -0
      src/commands/showLineBlame.ts
  22. +0
    -28
      src/commands/toggleBlame.ts
  23. +35
    -0
      src/commands/toggleFileBlame.ts
  24. +34
    -0
      src/commands/toggleLineBlame.ts
  25. +239
    -71
      src/configuration.ts
  26. +437
    -0
      src/currentLineController.ts
  27. +10
    -8
      src/extension.ts
  28. +160
    -0
      src/git/formatters/commit.ts
  29. +7
    -2
      src/git/models/diff.ts
  30. +1
    -1
      src/git/parsers/blameParser.ts
  31. +13
    -7
      src/git/parsers/diffParser.ts
  32. +1
    -1
      src/git/parsers/logParser.ts
  33. +57
    -52
      src/gitCodeLensProvider.ts
  34. +10
    -10
      src/gitService.ts
  35. +1
    -2
      src/quickPicks/common.ts
  36. +4
    -0
      src/system/function.ts
  37. +6
    -0
      src/system/object.ts
  38. +76
    -0
      src/system/string.ts
  39. +2
    -2
      tsconfig.json
  40. +1
    -1
      tslint.json

+ 128
- 48
README.md Переглянути файл

@ -5,56 +5,67 @@
# GitLens
GitLens **supercharges** the built-in Visual Studio Code Git capabilities. It helps you to **visualize code authorship** at a glance via inline Git blame annotations and code lens, **seamlessly navigate and explore** the history of a file or branch, **gain valuable insights** via powerful comparision commands, and so much more.
GitLens **supercharges** the built-in Visual Studio Code Git capabilities. It helps you to **visualize code authorship** at a glance via Git blame annotations and code lens, **seamlessly navigate and explore** the history of a file or branch, **gain valuable insights** via powerful comparision commands, and so much more.
GitLens provides an unobtrusive blame annotation at the end of the selected line, a status bar item showing the commit author and date of the selected line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the selected line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings).
GitLens provides an unobtrusive blame annotation at the end of the current line, a status bar item showing the commit author and date of the current line, code lens showing the most recent commit and # of authors of the file and/or code block, and many commands for exploring commits and histories, comparing and navigating revisions, stash access, repository status, and more. GitLens is also [highly customizable](#extension-settings) to meet your specific needs — find code lens intrusive or the current line blame annotation distracting — no problem, it is easy to [turn them off or change how they behave](#extension-settings).
## Previews
#### Featuring code lens, whole file inline blame annotations, and navigation and exploration via quick pick menus
#### Featuring code lens, file blame annotations, and navigation and exploration via quick pick menus
![GitLens preview 1](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview1.gif)
#### Featuring selected line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more
#### Featuring current line blame annotation and hovers, status bar commit details, quick pick menus, compare with previous, and more
![GitLens preview 2](https://raw.githubusercontent.com/eamodio/vscode-git-codelens/master/images/gitlens-preview2.gif)
## Features
#### Git Blame Annotations
- Adds a **blame annotation** to the end of the selected line showing the commit id and message, with more details (including the line's previous version) in a hover popup ([optional](#extension-settings), on by default)
- Adds a `Toggle Blame Annotations` command (`gitlens.toggleBlame`) with a shortcut of `alt+b` to toggle **inline Git blame annotations** for a whole file with multiple styles — compact, expanded, and trailing
- Also adds a `Show Blame Annotations` command (`gitlens.showBlame`)
- Adds **author and date blame information** about the selected line to the **status bar** ([optional](#extension-settings), on by default)
- By default clicking on the status bar shows a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Provides [customizable](#extension-settings) click behavior of the status bar — choose between one of the following
- Toggle whole file blame annotations on and off
- Toggle code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set
- Compare the file with the previous commit
- Show a quick pick menu with details and commands for the commit
- Adds a [customizable](#line-blame-annotation-settings) **Git blame annotation** to the end of the current line ([optional](#line-blame-annotation-settings), on by default)
- Contains the commit author, date, and message, by [default](#line-blame-annotation-settings)
- Commit details, including the changes from the line's previous version, are provided in a hover popup ([optional](#line-blame-annotation-settings), on by default)
- Adds on-demand, highly [customizable](#file-blame-annotation-settings) **Git blame annotations** of the whole file
- Choose between `gutter` (default) and `hover` [annotation styles](#file-blame-annotation-settings)
- Contains the commit message and date, by [default](#file-blame-annotation-settings)
- Commit details are also provided in a hover popup ([optional](#file-blame-annotation-settings), on by default)
- Adds [customizable](#status-bar-settings) **blame information** about the current line to the **status bar** ([optional](#status-bar-settings), on by default)
- Contains the commit author and date, by [default](#status-bar-settings)
- Clicking the status bar item will, by [default](#status-bar-settings), show a **commit details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Provides [customizable](#status-bar-settings) click behavior — choose between one of the following
- Toggle file blame annotations on and off
- Toggle code lens on and off
- Compare the line commit with the previous commit
- Compare the line commit with the working tree
- Show a quick pick menu with details and commands for the commit (default)
- Show a quick pick menu with file details and commands for the commit
- Show a quick pick menu with the commit history of the file
- Show a quick pick menu with the commit history of the current branch
- Adds a `Toggle File Blame Annotations` command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the file blame annotations on and off
- Also adds a `Show File Blame Annotations` command (`gitlens.showFileBlame`)
- Adds a `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) to toggle the current line blame annotations on and off
- Also adds a `Show Line Blame Annotations` command (`gitlens.showLineBlame`)
#### Git Code Lens
- Adds **code lens** to the top of the file and on code blocks ([optional](#extension-settings), on by default)
- Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default)
- **Recent Change** — author and date of the most recent commit for the file or code block
- By default, clicking on the code lens shows a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- Clicking the code lens will, by [default](#code-lens-settings), show a **commit file details quick pick menu** with commands for comparing, navigating and exploring commits, and more
- **Authors** — number of authors of the file or code block and the most prominent author (if there is more than one)
- By default, clicking on the code lens toggles the inline Git blame annotations on and off for the whole file
- Clicking the code lens will, by [default](#code-lens-settings), toggle the file Git blame annotations on and off of the whole file
- Will be hidden if the author of the most recent commit is also the only author of the file or block, to avoid duplicate information and reduce visual noise
- Provides [customizable](#extension-settings) click behavior for each code lens — choose between one of the following
- Toggle whole file blame annotations on and off
- Compare the file with the previous commit
- Provides [customizable](#code-lens-settings) click behavior for each code lens — choose between one of the following
- Toggle file blame annotations on and off
- Compare the commit with the previous commit
- Show a quick pick menu with details and commands for the commit
- Show a quick pick menu with file details and commands for the commit
- Show a quick pick menu with the commit history of the file
- Show a quick pick menu with the commit history of the current branch
- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off — only available if [`"gitlens.codeLens.visibility": "ondemand"`](#extension-settings) is set
- Adds a `Toggle Git Code Lens` command (`gitlens.toggleCodeLens`) with a shortcut of `shift+alt+b` to toggle the code lens on and off
#### Powerful Comparison Tools
@ -180,40 +191,109 @@ GitLens provides an unobtrusive blame annotation at the end of the selected line
## Insiders
Add [`"gitlens.insiders": true`](#extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues.
Add [`"gitlens.insiders": true`](#general-extension-settings) to your settings to join the insiders channel and get early access to upcoming features. Be aware that because this provides early access expect there to be issues.
## Extension Settings
GitLens is highly customizable and provides many configuration settings to allow the personalization of almost all features
### General Settings
|Name | Description
|-----|------------
|`gitlens.insiders`|Opts into the insiders channel -- provides access to upcoming features
|`gitlens.outputLevel`|Specifies how much (if any) output will be sent to the GitLens output channel
|`gitlens.blame.annotation.activeLine`|Specifies whether and how to show blame annotations on the active line. `off` - no annotation. `inline` - adds a trailing annotation to the active line. `hover` - adds hover annotation to the active line. `both` - adds both `inline` and `hover` annotations
|`gitlens.blame.annotation.activeLineDarkColor`|Specifies the color of the active line blame annotation to use with a dark theme. Must be a valid css color
|`gitlens.blame.annotation.activeLineLightColor`|Specifies the color of the active line blame annotation to use with a light theme. Must be a valid css color
|`gitlens.blame.annotation.highlight`|Specifies whether and how to highlight blame annotations. `none` - no highlight. `gutter` - adds a gutter icon. `line` - adds a full-line highlight. `both` - adds both `gutter` and `line` highlights
|`gitlens.blame.annotation.style`|Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation on every line
|`gitlens.blame.annotation.author`|Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.date`|Specifies whether and how the commit date will be shown in the blame annotations. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.blame.annotation.dateFormat`. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.blame.annotation.message`|Specifies whether the commit message will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.blame.annotation.sha`|Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles
|`gitlens.codeLens.visibility`|Specifies when code lens will be shown in the active document. `auto` - always shown. `ondemand` - never shown, unless toggled via the `gitlens.toggleCodeLens` command. `off` - never shown
|`gitlens.codeLens.authors.enabled`|Specifies whether the authors code lens is shown
|`gitlens.codeLens.authors.command`|Specifies the command executed when the authors code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.recentChange.enabled`|Specifies whether the recent change code lens is shown
|`gitlens.codeLens.recentChange.command`|"Specifies the command executed when the recent change code lens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.location`|Specifies where code lens will be rendered in the active document. `all` - render at the top of the document, on container-like (classes, modules, etc), and on member-like (methods, functions, properties, etc) lines. `document+containers` - render at the top of the document and on container-like lines. `document` - only render at the top of the document. `custom` - rendering controlled by `gitlens.codeLens.locationCustomSymbols`
|`gitlens.codeLens.locationCustomSymbols`|Specifies the set of document symbols to render active document code lens on. Must be a member of `SymbolKind`
|`gitlens.codeLens.languageLocations`|Specifies where code lens will be rendered in the active document for the specified languages
|`gitlens.menus.diff.enabled`|Specifies whether diff commands will be added to the context menus
|`gitlens.statusBar.enabled`|Specifies whether blame information is shown in the status bar
### Blame Annotation Settings
#### File Blame Annotation Settings
|Name | Description
|-----|------------
|`gitlens.blame.file.annotationType`|Specifies the type of blame annotations that will be shown for the current file. `gutter` - adds an annotation to the beginning of each line. `hover` - shows annotations when hovering over each line
|`gitlens.blame.file.lineHighlight.enabled`|Specifies whether or not to highlight lines associated with the current line
|`gitlens.blame.file.lineHighlight.locations`|Specifies where the associated line highlights will be shown. `gutter` - adds a gutter glyph. `line` - adds a full-line highlight background color. `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`gitlens.annotations.file.gutter.format`|Specifies the format of the gutter blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.file.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.annotations.file.gutter.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in gutter blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.annotations.file.gutter.compact`|Specifies whether or not to compact (deduplicate) matching adjacent gutter blame annotations
|`gitlens.annotations.file.gutter.heatmap.enabled`|Specifies whether or not to provide a heatmap indicator in the gutter blame annotations
|`gitlens.annotations.file.gutter.heatmap.location`|Specifies where the heatmap indicators will be shown in the gutter blame annotations. `left` - adds a heatmap indicator on the left edge of the gutter blame annotations. `right` - adds a heatmap indicator on the right edge of the gutter blame annotations
|`gitlens.annotations.file.gutter.hover.details`|Specifies whether or not to provide a commit details hover annotation over the gutter blame annotations
|`gitlens.annotations.file.gutter.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.annotations.file.hover.heatmap.enabled`|Specifies whether or not to provide heatmap indicators on the left edge of each line
|`gitlens.annotations.file.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
#### Line Blame Annotation Settings
|Name | Description
|-----|------------
|`gitlens.blame.line.enabled`|Specifies whether or not to provide a blame annotation for the current line
|`gitlens.blame.line.annotationType`|Specifies the type of blame annotations that will be shown for the current line. `trailing` - adds an annotation to the end of the current line. `hover` - shows annotations when hovering over the current line
|`gitlens.annotations.line.trailing.format`|Specifies the format of the trailing blame annotations. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.annotations.currentLine.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.annotations.line.trailing.dateFormat`|Specifies how to format absolute dates (using the `${date}` token) in trailing blame annotations. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.annotations.line.trailing.hover.details`|Specifies whether or not to provide a commit details hover annotation over the trailing blame annotations
|`gitlens.annotations.line.trailing.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation over the trailing blame annotations
|`gitlens.annotations.line.trailing.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
|`gitlens.annotations.line.hover.details`|Specifies whether or not to provide a commit details hover annotation for the current line
|`gitlens.annotations.line.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation for the current line
### Code Lens Settings
|Name | Description
|-----|------------
|`gitlens.codeLens.enabled`|Specifies whether or not to provide any Git code lens
|`gitlens.codeLens.recentChange.enabled`|Specifies whether or not to show a `recent change` code lens showing the author and date of the most recent commit for the file or code block
|`gitlens.codeLens.recentChange.command`|Specifies the command to be executed when the `recent change` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.authors.enabled`|Specifies whether or not to show an `authors` code lens showing number of authors of the file or code block and the most prominent author (if there is more than one)
|`gitlens.codeLens.authors.command`|Specifies the command to be executed when the `authors` code lens is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current committed file with the previous commit. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.codeLens.locations`|Specifies where Git code lens will be shown in the document. `document` - adds code lens at the top of the document. `containers` - adds code lens at the start of container-like symbols (modules, classes, interfaces, etc). `blocks` - adds code lens at the start of block-like symbols (functions, methods, properties, etc) lines. `custom` - adds code lens at the start of symbols contained in `gitlens.codeLens.locationCustomSymbols`
|`gitlens.codeLens.customLocationSymbols`|Specifies the set of document symbols where Git code lens will be shown in the document
|`gitlens.codeLens.perLanguageLocations`|Specifies where Git code lens will be shown in the document for the specified languages
### Status Bar Settings
|Name | Description
|-----|------------
|`gitlens.statusBar.enabled`|Specifies whether or not to provide blame information on the status bar
|`gitlens.statusBar.alignment`|Specifies the blame alignment in the status bar. `left` - align to the left, `right` - align to the right
|`gitlens.statusBar.command`|Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.statusBar.date`|Specifies whether and how the commit date will be shown in the blame status bar. `off` - no date. `relative` - relative date (e.g. 1 day ago). `absolute` - date format specified by `gitlens.statusBar.dateFormat`
|`gitlens.statusBar.dateFormat`|Specifies the date format of how absolute dates will be shown in the blame status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats
|`gitlens.statusBar.format`|Specifies the format of the blame information on the status bar. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
|`gitlens.statusBar.command`|Specifies the command to be executed when the blame status bar item is clicked. `gitlens.toggleFileBlame` - toggles file blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current line commit with the previous. `gitlens.diffWithWorking` - compares the current line commit with the working tree. `gitlens.toggleCodeLens` - toggles Git code lens. `gitlens.showQuickCommitDetails` - shows a commit details quick pick. `gitlens.showQuickCommitFileDetails` - shows a commit file details quick pick. `gitlens.showQuickFileHistory` - shows a file history quick pick. `gitlens.showQuickRepoHistory` - shows a branch history quick pick
|`gitlens.statusBar.format`|Specifies the format of the status bar blame information. Available tokens: `${id}` - commit id, `${author}` - commit author, `${message}` - commit message, `${ago}` - relative commit date (e.g. 1 day ago), `${date}` - formatted commit date (format specified by `gitlens.statusBar.dateFormat`), `${authorAgo}` - commit author, relative commit date
|`gitlens.statusBar.dateFormat`|Specifies the date format of absolute dates shown in the blame information on the status bar. See https://momentjs.com/docs/#/displaying/format/ for valid formats
### Theme Settings
|Name | Description
|-----|------------
|`gitlens.theme.annotations.file.gutter.separateLines`|Specifies whether or not gutter blame annotations will be separated by a small gap
|`gitlens.theme.annotations.file.gutter.dark.backgroundColor`|Specifies the dark theme background color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.backgroundColor`|Specifies the light theme background color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.dark.foregroundColor`|Specifies the dark theme foreground color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.foregroundColor`|Specifies the light theme foreground color of the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.dark.uncommittedForegroundColor`|Specifies the dark theme foreground color of an uncommitted line in the gutter blame annotations
|`gitlens.theme.annotations.file.gutter.light.uncommittedForegroundColor`|Specifies the light theme foreground color of an uncommitted line in the gutter blame annotations
|`gitlens.theme.annotations.file.hover.separateLines`|Specifies whether or not hover blame annotations will be separated by a small gap (if heatmap is enabled)
|`gitlens.theme.annotations.line.trailing.dark.backgroundColor`|Specifies the dark theme background color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.light.backgroundColor`|Specifies the light theme background color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.dark.foregroundColor`|Specifies the dark theme foreground color of the trailing blame annotation
|`gitlens.theme.annotations.line.trailing.light.foregroundColor`|Specifies the light theme foreground color of the trailing blame annotation
|`gitlens.theme.lineHighlight.dark.backgroundColor`|Specifies the dark theme background color of the associated line highlights in blame annotations. Must be a valid css color
|`gitlens.theme.lineHighlight.light.backgroundColor`|Specifies the light theme background color of the associated line highlights in blame annotations. Must be a valid css color
|`gitlens.theme.lineHighlight.dark.overviewRulerColor`|Specifies the dark theme overview ruler color of the associated line highlights in blame annotations
|`gitlens.theme.lineHighlight.light.overviewRulerColor`|Specifies the light theme overview ruler color of the associated line highlights in blame annotations
### Advanced Settings
|Name | Description
|-----|------------
|`gitlens.advanced.toggleWhitespace.enabled`|Specifies whether or not to toggle whitespace off then showing blame annotations (*may* be required by certain fonts/themes)
|`gitlens.advanced.menus`|Specifies which commands will be added to the menus
|`gitlens.advanced.caching.enabled`|Specifies whether git output will be cached
|`gitlens.advanced.caching.maxLines`|Specifies the threshold for caching larger documents
|`gitlens.advanced.git`|Specifies the git path to use
|`gitlens.advanced.gitignore.enabled`|Specifies whether or not to parse the root .gitignore file for better performance (i.e. avoids blaming excluded files)
|`gitlens.advanced.maxQuickHistory`|Specifies the maximum number of QuickPick history entries to show
|`gitlens.advanced.quickPick.closeOnFocusOut`|Specifies whether or not to close the QuickPick menu when focus is lost
## Known Issues

+ 1
- 1
images/blame-dark.svg Переглянути файл

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

+ 1
- 1
images/blame-light.svg Переглянути файл

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

+ 1263
- 929
package.json
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 282
- 0
src/annotations/annotationController.ts Переглянути файл

@ -0,0 +1,282 @@
'use strict';
import { Functions, Objects } from '../system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { TextDocumentComparer, TextEditorComparer } from '../comparers';
import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
import { Logger } from '../logger';
import { WhitespaceController } from './whitespaceController';
export const Decorations = {
annotation: window.createTextEditorDecorationType({
isWholeLine: true
} as DecorationRenderOptions),
highlight: undefined as TextEditorDecorationType | undefined
};
export class AnnotationController extends Disposable {
private _onDidToggleAnnotations = new EventEmitter<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);
}
}
}

+ 74
- 0
src/annotations/annotationProvider.ts Переглянути файл

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

+ 189
- 0
src/annotations/annotations.ts Переглянути файл

@ -0,0 +1,189 @@
import { DecorationInstanceRenderOptions, DecorationOptions, ThemableDecorationRenderOptions } from 'vscode';
import { IThemeConfig, themeDefaults } from '../configuration';
import { CommitFormatter, GitCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
interface IHeatmapConfig {
enabled: boolean;
location?: 'left' | 'right';
}
interface IRenderOptions {
uncommittedForegroundColor?: {
dark: string;
light: string;
};
before?: DecorationInstanceRenderOptions & ThemableDecorationRenderOptions & { height?: string };
dark?: DecorationInstanceRenderOptions;
light?: DecorationInstanceRenderOptions;
}
export const endOfLineIndex = 1000000;
export class Annotations {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) {
const color = this._getHeatmapColor(now, date);
(decoration.renderOptions!.before! as any).borderColor = color;
}
private static _getHeatmapColor(now: moment.Moment, date: Date) {
const days = now.diff(moment(date), 'days');
if (days <= 2) return '#ffeca7';
if (days <= 7) return '#ffdd8c';
if (days <= 14) return '#ffdd7c';
if (days <= 30) return '#fba447';
if (days <= 60) return '#f68736';
if (days <= 90) return '#f37636';
if (days <= 180) return '#ca6632';
if (days <= 365) return '#c0513f';
if (days <= 730) return '#a2503a';
return '#793738';
}
static async changesHover(commit: GitCommit, line: number, uri: GitUri, git: GitService): Promise<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 } };
}
}

+ 82
- 0
src/annotations/blameAnnotationProvider.ts Переглянути файл

@ -0,0 +1,82 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { GitService, GitUri, IGitBlame } from '../gitService';
import { WhitespaceController } from './whitespaceController';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
protected _blame: Promise<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;
}
}

+ 69
- 0
src/annotations/diffAnnotationProvider.ts Переглянути файл

@ -0,0 +1,69 @@
'use strict';
import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { GitService, GitUri } from '../gitService';
import { WhitespaceController } from './whitespaceController';
export class DiffAnnotationProvider extends AnnotationProviderBase {
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, private git: GitService, private uri: GitUri) {
super(context, editor, decoration, highlightDecoration, whitespaceController);
}
async provideAnnotation(shaOrLine?: string | number): Promise<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;
}
}

+ 76
- 0
src/annotations/gutterBlameAnnotationProvider.ts Переглянути файл

@ -0,0 +1,76 @@
'use strict';
import { Strings } from '../system';
import { DecorationOptions, Range } from 'vscode';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { Annotations, endOfLineIndex } from './annotations';
import { FileAnnotationType } from '../configuration';
import { ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise<boolean> {
this.annotationType = FileAnnotationType.Gutter;
const blame = await this.getBlame(true);
if (blame === undefined) return false;
const cfg = this._config.annotations.file.gutter;
// Precalculate the formatting options so we don't need to do it on each iteration
const tokenOptions = Strings.getTokensFromTemplate(cfg.format)
.reduce((map, token) => {
map[token.key] = token.options;
return map;
}, {} as { [token: string]: ICommitFormatOptions });
const options: ICommitFormatOptions = {
dateFormat: cfg.dateFormat,
tokenOptions: tokenOptions
};
const now = moment();
const offset = this.uri.offset;
let previousLine: string | undefined = undefined;
const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap);
const decorations: DecorationOptions[] = [];
for (const l of blame.lines) {
const commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
const line = l.line + offset;
const gutter = Annotations.gutter(commit, cfg.format, options, renderOptions, cfg.compact && previousLine === l.sha);
if (cfg.compact) {
const isEmptyOrWhitespace = this.document.lineAt(line).isEmptyOrWhitespace;
previousLine = isEmptyOrWhitespace ? undefined : l.sha;
}
if (cfg.heatmap.enabled) {
Annotations.applyHeatmap(gutter, commit.date, now);
}
const firstNonWhitespace = this.editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
gutter.range = this.editor.document.validateRange(new Range(line, 0, line, firstNonWhitespace));
decorations.push(gutter);
if (cfg.hover.details) {
const details = Annotations.detailsHover(commit);
details.range = cfg.hover.wholeLine
? this.editor.document.validateRange(new Range(line, 0, line, endOfLineIndex))
: gutter.range;
decorations.push(details);
}
}
if (decorations.length) {
this.editor.setDecorations(this.decoration, decorations);
}
this.selection(shaOrLine, blame);
return true;
}
}

+ 49
- 0
src/annotations/hoverBlameAnnotationProvider.ts Переглянути файл

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

src/whitespaceController.ts → src/annotations/whitespaceController.ts Переглянути файл

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

+ 0
- 391
src/blameActiveLineController.ts Переглянути файл

@ -1,391 +0,0 @@
'use strict';
import { Functions, Objects } from './system';
import { DecorationInstanceRenderOptions, DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationController } from './blameAnnotationController';
import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter';
import { Commands } from './commands';
import { TextEditorComparer } from './comparers';
import { IBlameConfig, IConfig, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService';
import * as moment from 'moment';
const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions);
export class BlameActiveLineController extends Disposable {
private _activeEditorLineDisposable: Disposable | undefined;
private _blameable: boolean;
private _config: IConfig;
private _currentLine: number = -1;
private _disposable: Disposable;
private _editor: TextEditor | undefined;
private _statusBarItem: StatusBarItem | undefined;
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>;
private _uri: GitUri;
constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: BlameAnnotationController) {
super(() => this.dispose());
this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250);
this._onConfigurationChanged();
const subscriptions: Disposable[] = [];
subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
subscriptions.push(git.onDidChangeGitCache(this._onGitCacheChanged, this));
subscriptions.push(annotationController.onDidToggleBlameAnnotations(this._onBlameAnnotationToggled, this));
this._disposable = Disposable.from(...subscriptions);
}
dispose() {
this._editor && this._editor.setDecorations(activeLineDecoration, []);
this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose();
this._disposable && this._disposable.dispose();
}
private _onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
let changed = false;
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) {
changed = true;
if (cfg.statusBar.enabled) {
const alignment = cfg.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left;
if (this._statusBarItem !== undefined && this._statusBarItem.alignment !== alignment) {
this._statusBarItem.dispose();
this._statusBarItem = undefined;
}
this._statusBarItem = this._statusBarItem || window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0);
this._statusBarItem.command = cfg.statusBar.command;
}
else if (!cfg.statusBar.enabled && this._statusBarItem) {
this._statusBarItem.dispose();
this._statusBarItem = undefined;
}
}
if (!Objects.areEquivalent(cfg.blame.annotation.activeLine, this._config && this._config.blame.annotation.activeLine)) {
changed = true;
if (cfg.blame.annotation.activeLine !== 'off' && this._editor) {
this._editor.setDecorations(activeLineDecoration, []);
}
}
if (!Objects.areEquivalent(cfg.blame.annotation.activeLineDarkColor, this._config && this._config.blame.annotation.activeLineDarkColor) ||
!Objects.areEquivalent(cfg.blame.annotation.activeLineLightColor, this._config && this._config.blame.annotation.activeLineLightColor)) {
changed = true;
}
this._config = cfg;
if (!changed) return;
const trackActiveLine = cfg.statusBar.enabled || cfg.blame.annotation.activeLine !== 'off';
if (trackActiveLine && !this._activeEditorLineDisposable) {
const subscriptions: Disposable[] = [];
subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this));
subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this));
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
this._activeEditorLineDisposable = Disposable.from(...subscriptions);
}
else if (!trackActiveLine && this._activeEditorLineDisposable) {
this._activeEditorLineDisposable.dispose();
this._activeEditorLineDisposable = undefined;
}
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private isEditorBlameable(editor: TextEditor | undefined): boolean {
if (editor === undefined || editor.document === undefined) return false;
if (!this.git.isTrackable(editor.document.uri)) return false;
if (editor.document.isUntitled && editor.document.uri.scheme === DocumentSchemes.File) return false;
return this.git.isEditorBlameable(editor);
}
private async _onActiveTextEditorChanged(editor: TextEditor | undefined) {
this._currentLine = -1;
const previousEditor = this._editor;
previousEditor && previousEditor.setDecorations(activeLineDecoration, []);
if (editor === undefined || !this.isEditorBlameable(editor)) {
this.clear(editor);
this._editor = undefined;
return;
}
this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty;
this._editor = editor;
this._uri = await GitUri.fromUri(editor.document.uri, this.git);
const maxLines = this._config.advanced.caching.statusBar.maxLines;
// If caching is on and the file is small enough -- kick off a blame for the whole file
if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) {
this.git.getBlameForFile(this._uri);
}
this._updateBlameDebounced(editor.selection.active.line, editor);
}
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
this._blameable = e.blameable;
if (!e.blameable || !this._editor) {
this.clear(e.editor);
return;
}
// Make sure this is for the editor we are tracking
if (!TextEditorComparer.equals(this._editor, e.editor)) return;
this._updateBlameDebounced(this._editor.selection.active.line, this._editor);
}
private _onBlameAnnotationToggled() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private _onGitCacheChanged() {
this._onActiveTextEditorChanged(window.activeTextEditor);
}
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<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.command = Commands.DiffLineWithPrevious;
this._statusBarItem.tooltip = 'Compare File with Previous';
break;
case StatusBarCommand.DiffWithWorking:
this._statusBarItem.command = Commands.DiffLineWithWorking;
this._statusBarItem.tooltip = 'Compare File with Working Tree';
break;
case StatusBarCommand.ToggleCodeLens:
this._statusBarItem.tooltip = 'Toggle Git CodeLens';
break;
case StatusBarCommand.ShowQuickCommitDetails:
this._statusBarItem.tooltip = 'Show Commit Details';
break;
case StatusBarCommand.ShowQuickCommitFileDetails:
this._statusBarItem.tooltip = 'Show Line Commit Details';
break;
case StatusBarCommand.ShowQuickFileHistory:
this._statusBarItem.tooltip = 'Show File History';
break;
case StatusBarCommand.ShowQuickCurrentBranchHistory:
this._statusBarItem.tooltip = 'Show Branch History';
break;
}
this._statusBarItem.show();
}
if (this._config.blame.annotation.activeLine !== 'off') {
const activeLine = this._config.blame.annotation.activeLine;
const offset = this._uri.offset;
const cfg = {
annotation: {
sha: true,
author: this._config.statusBar.enabled ? false : this._config.blame.annotation.author,
date: this._config.statusBar.enabled ? 'off' : this._config.blame.annotation.date,
message: true
}
} as IBlameConfig;
const annotation = BlameAnnotationFormatter.getAnnotation(cfg, commit, BlameAnnotationFormat.Unconstrained);
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
if (!commit.isUncommitted) {
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha);
}
// I have no idea why I need this protection -- but it happens
if (!editor.document) return;
let hoverMessage: string | string[] | undefined = undefined;
if (activeLine !== 'inline') {
// If the messages match (or we couldn't find the log), then this is a possible duplicate annotation
const possibleDuplicate = !logCommit || logCommit.message === commit.message;
// If we don't have a possible dupe or we aren't showing annotations get the hover message
if (!commit.isUncommitted && (!possibleDuplicate || !this.annotationController.isAnnotating(editor))) {
hoverMessage = BlameAnnotationFormatter.getAnnotationHover(cfg, blameLine, logCommit || commit);
if (commit.previousSha !== undefined) {
const changes = await this.git.getDiffForLine(this._uri, blameLine.line + offset, commit.previousSha);
if (changes !== undefined) {
let previous = changes[0];
if (previous !== undefined) {
previous = previous.replace(/\n/g, '\`\n>\n> \`').trim();
hoverMessage += `\n\n---\n\`\`\`\n${previous}\n\`\`\``;
}
}
}
}
else if (commit.isUncommitted) {
const changes = await this.git.getDiffForLine(this._uri, blameLine.line + offset);
if (changes !== undefined) {
let previous = changes[0];
if (previous !== undefined) {
previous = previous.replace(/\n/g, '\`\n>\n> \`').trim();
hoverMessage = `\`${'0'.repeat(8)}\` &nbsp; __Uncommitted change__\n\n---\n\`\`\`\n${previous}\n\`\`\``;
}
}
}
}
let decorationOptions: [DecorationOptions] | undefined = undefined;
switch (activeLine) {
case 'both':
case 'inline':
const range = editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000));
decorationOptions = [
{
range: range.with({
start: range.start.with({
character: range.end.character
})
}),
hoverMessage: hoverMessage,
renderOptions: {
after: {
contentText: annotation
},
dark: {
after: {
color: this._config.blame.annotation.activeLineDarkColor || 'rgba(153, 153, 153, 0.35)'
}
},
light: {
after: {
color: this._config.blame.annotation.activeLineLightColor || 'rgba(153, 153, 153, 0.35)'
}
}
} as DecorationInstanceRenderOptions
} as DecorationOptions
];
if (activeLine === 'both') {
// Add a hover decoration to the area between the start of the line and the first non-whitespace character
decorationOptions.push({
range: range.with({
end: range.end.with({
character: editor.document.lineAt(range.end.line).firstNonWhitespaceCharacterIndex
})
}),
hoverMessage: hoverMessage
} as DecorationOptions);
}
break;
case 'hover':
decorationOptions = [
{
range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)),
hoverMessage: hoverMessage
} as DecorationOptions
];
break;
}
if (decorationOptions !== undefined) {
editor.setDecorations(activeLineDecoration, decorationOptions);
}
}
}
}

+ 0
- 271
src/blameAnnotationController.ts Переглянути файл

@ -1,271 +0,0 @@
'use strict';
import { Functions } from './system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationProvider } from './blameAnnotationProvider';
import { TextDocumentComparer, TextEditorComparer } from './comparers';
import { IBlameConfig } from './configuration';
import { ExtensionKey } from './constants';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from './gitService';
import { Logger } from './logger';
import { WhitespaceController } from './whitespaceController';
export const BlameDecorations = {
annotation: window.createTextEditorDecorationType({
before: {
margin: '0 1.75em 0 0'
},
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions),
highlight: undefined as TextEditorDecorationType | undefined
};
export class BlameAnnotationController extends Disposable {
private _onDidToggleBlameAnnotations = new EventEmitter<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.onDidChangeTextDocument(this._onTextDocumentChanged, this));
subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));
this._blameAnnotationsDisposable = Disposable.from(...subscriptions);
}
this._annotationProviders.set(editor.viewColumn || -1, provider);
if (await provider.provideBlameAnnotation(shaOrLine)) {
this._onDidToggleBlameAnnotations.fire();
return true;
}
return false;
}
isAnnotating(editor: TextEditor): boolean {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
return !!this._annotationProviders.get(editor.viewColumn || -1);
}
async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
const provider = this._annotationProviders.get(editor.viewColumn || -1);
if (!provider) return this.showBlameAnnotation(editor, shaOrLine);
await this.clear(provider.editor.viewColumn || -1);
return false;
}
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
if (e.blameable || !e.editor) return;
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;
Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`);
this.clear(key);
}
}
private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e.document)) continue;
// We have to defer because isDirty is not reliable inside this event
setTimeout(() => {
// If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
if (e.document.isDirty) return;
// If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
// Which means the document has been reloaded and the blame annotations have been removed, so we need to update (clear) our state tracking
Logger.log('TextDocumentChanged:', `Clear blame annotations for column ${key}`);
this.clear(key);
}, 1);
}
}
private _onTextDocumentClosed(e: TextDocument) {
for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e)) continue;
Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`);
this.clear(key);
}
}
private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
const viewColumn = e.viewColumn || -1;
Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`);
await this.clear(viewColumn);
for (const [key, p] of this._annotationProviders) {
if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;
Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`);
await this.clear(key);
}
}
private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;
for (const [key, p] of this._annotationProviders) {
if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;
Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`);
this.clear(key);
}
}
}

+ 0
- 113
src/blameAnnotationFormatter.ts Переглянути файл

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

+ 0
- 302
src/blameAnnotationProvider.ts Переглянути файл

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

+ 5
- 3
src/commands.ts Переглянути файл

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

+ 7
- 5
src/commands/common.ts Переглянути файл

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

+ 0
- 28
src/commands/showBlame.ts Переглянути файл

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

+ 35
- 0
src/commands/showFileBlame.ts Переглянути файл

@ -0,0 +1,35 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
import { Logger } from '../logger';
export interface ShowFileBlameCommandArgs {
sha?: string;
type?: FileAnnotationType;
}
export class ShowFileBlameCommand extends EditorCommand {
constructor(private annotationController: AnnotationController) {
super(Commands.ShowFileBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise<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`);
}
}
}

+ 34
- 0
src/commands/showLineBlame.ts Переглянути файл

@ -0,0 +1,34 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { CurrentLineController } from '../currentLineController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration';
import { Logger } from '../logger';
export interface ShowLineBlameCommandArgs {
type?: LineAnnotationType;
}
export class ShowLineBlameCommand extends EditorCommand {
constructor(private currentLineController: CurrentLineController) {
super(Commands.ShowLineBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise<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`);
}
}
}

+ 0
- 28
src/commands/toggleBlame.ts Переглянути файл

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

+ 35
- 0
src/commands/toggleFileBlame.ts Переглянути файл

@ -0,0 +1,35 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration';
import { Logger } from '../logger';
export interface ToggleFileBlameCommandArgs {
sha?: string;
type?: FileAnnotationType;
}
export class ToggleFileBlameCommand extends EditorCommand {
constructor(private annotationController: AnnotationController) {
super(Commands.ToggleFileBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise<any> {
if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined;
try {
if (args.type === undefined) {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
args.type = cfg.blame.file.annotationType;
}
return this.annotationController.toggleAnnotations(editor, args.type, args.sha !== undefined ? args.sha : editor.selection.active.line);
}
catch (ex) {
Logger.error(ex, 'ToggleFileBlameCommand');
return window.showErrorMessage(`Unable to toggle file blame annotations. See output channel for more details`);
}
}
}

+ 34
- 0
src/commands/toggleLineBlame.ts Переглянути файл

@ -0,0 +1,34 @@
'use strict';
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { CurrentLineController } from '../currentLineController';
import { Commands, EditorCommand } from './common';
import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration';
import { Logger } from '../logger';
export interface ToggleLineBlameCommandArgs {
type?: LineAnnotationType;
}
export class ToggleLineBlameCommand extends EditorCommand {
constructor(private currentLineController: CurrentLineController) {
super(Commands.ToggleLineBlame);
}
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise<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`);
}
}
}

+ 239
- 71
src/configuration.ts Переглянути файл

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

+ 437
- 0
src/currentLineController.ts Переглянути файл

@ -0,0 +1,437 @@
'use strict';
import { Functions, Objects } from './system';
import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode';
import { AnnotationController } from './annotations/annotationController';
import { Annotations, endOfLineIndex } from './annotations/annotations';
import { Commands } from './commands';
import { TextEditorComparer } from './comparers';
import { FileAnnotationType, IConfig, LineAnnotationType, StatusBarCommand } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: {
margin: '0 0 0 4em'
}
} as DecorationRenderOptions);
export class CurrentLineController extends Disposable {
private _activeEditorLineDisposable: Disposable | undefined;
private _blameable: boolean;
private _config: IConfig;
private _currentLine: number = -1;
private _disposable: Disposable;
private _editor: TextEditor | undefined;
private _statusBarItem: StatusBarItem | undefined;
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<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();
}
}

+ 10
- 8
src/extension.ts Переглянути файл

@ -1,14 +1,13 @@
'use strict';
import { Objects } from './system';
import { commands, ExtensionContext, extensions, languages, Uri, window, workspace } from 'vscode';
import { BlameActiveLineController } from './blameActiveLineController';
import { BlameAnnotationController } from './blameAnnotationController';
import { AnnotationController } from './annotations/annotationController';
import { CommandContext, setCommandContext } from './commands';
import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands';
import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteCommand, OpenInRemoteCommand, OpenRepoInRemoteCommand } from './commands';
import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands';
import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands';
import { ShowBlameCommand, ToggleBlameCommand } from './commands';
import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleLineBlameCommand } from './commands';
import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands';
import { ShowLastQuickPickCommand } from './commands';
import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands';
@ -19,6 +18,7 @@ import { ToggleCodeLensCommand } from './commands';
import { Keyboard } from './commands';
import { IConfig } from './configuration';
import { ApplicationInsightsKey, BuiltInCommands, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants';
import { CurrentLineController } from './currentLineController';
import { GitContentProvider } from './gitContentProvider';
import { GitContextTracker, GitService } from './gitService';
import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
@ -74,11 +74,11 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git)));
const annotationController = new BlameAnnotationController(context, git, gitContextTracker);
const annotationController = new AnnotationController(context, git, gitContextTracker);
context.subscriptions.push(annotationController);
const activeLineController = new BlameActiveLineController(context, git, gitContextTracker, annotationController);
context.subscriptions.push(activeLineController);
const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController);
context.subscriptions.push(currentLineController);
context.subscriptions.push(new Keyboard());
@ -98,8 +98,10 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(new OpenFileInRemoteCommand(git));
context.subscriptions.push(new OpenInRemoteCommand());
context.subscriptions.push(new OpenRepoInRemoteCommand(git));
context.subscriptions.push(new ShowBlameCommand(annotationController));
context.subscriptions.push(new ToggleBlameCommand(annotationController));
context.subscriptions.push(new ShowFileBlameCommand(annotationController));
context.subscriptions.push(new ShowLineBlameCommand(currentLineController));
context.subscriptions.push(new ToggleFileBlameCommand(annotationController));
context.subscriptions.push(new ToggleLineBlameCommand(currentLineController));
context.subscriptions.push(new ShowBlameHistoryCommand(git));
context.subscriptions.push(new ShowFileHistoryCommand(git));
context.subscriptions.push(new ShowLastQuickPickCommand());

+ 160
- 0
src/git/formatters/commit.ts Переглянути файл

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

+ 7
- 2
src/git/models/diff.ts Переглянути файл

@ -1,11 +1,16 @@
'use strict';
export interface IGitDiffLine {
line: string;
state: 'added' | 'removed' | 'unchanged';
}
export interface IGitDiffChunk {
current: (string | undefined)[];
current: (IGitDiffLine | undefined)[];
currentStart: number;
currentEnd: number;
previous: (string | undefined)[];
previous: (IGitDiffLine | undefined)[];
previousStart: number;
previousEnd: number;

+ 1
- 1
src/git/parsers/blameParser.ts Переглянути файл

@ -60,7 +60,7 @@ export class GitBlameParser {
switch (lineParts[0]) {
case 'author':
entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted'
? 'You'
: lineParts.slice(1).join(' ').trim();
break;

+ 13
- 7
src/git/parsers/diffParser.ts Переглянути файл

@ -1,5 +1,5 @@
'use strict';
import { IGitDiff, IGitDiffChunk } from './../git';
import { IGitDiff, IGitDiffChunk, IGitDiffLine } from './../git';
const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm;
@ -21,23 +21,29 @@ export class GitDiffParser {
const chunk = match[5];
const lines = chunk.split('\n').slice(1);
const current = [];
const previous = [];
const current: (IGitDiffLine | undefined)[] = [];
const previous: (IGitDiffLine | undefined)[] = [];
for (const l of lines) {
switch (l[0]) {
case '+':
current.push(` ${l.substring(1)}`);
current.push({
line: ` ${l.substring(1)}`,
state: 'added'
});
previous.push(undefined);
break;
case '-':
current.push(undefined);
previous.push(` ${l.substring(1)}`);
previous.push({
line: ` ${l.substring(1)}`,
state: 'removed'
});
break;
default:
current.push(l);
previous.push(l);
current.push({ line: l, state: 'unchanged' });
previous.push({ line: l, state: 'unchanged' });
break;
}
}

+ 1
- 1
src/git/parsers/logParser.ts Переглянути файл

@ -61,7 +61,7 @@ export class GitLogParser {
switch (lineParts[0]) {
case 'author':
entry.author = Git.isUncommitted(entry.sha)
? 'Uncommitted'
? 'You'
: lineParts.slice(1).join(' ').trim();
break;

+ 57
- 52
src/gitCodeLensProvider.ts Переглянути файл

@ -3,7 +3,7 @@ import { Functions, Iterables, Strings } from './system';
import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, DocumentSelector, Event, EventEmitter, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands';
import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants';
import { CodeLensCommand, CodeLensLocation, ICodeLensLanguageLocation, IConfig } from './configuration';
import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration';
import { GitCommit, GitService, GitUri, IGitBlame, IGitBlameLines } from './gitService';
import { Logger } from './logger';
import * as moment from 'moment';
@ -56,24 +56,22 @@ export class GitCodeLensProvider implements CodeLensProvider {
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<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));
@ -117,55 +116,61 @@ export class GitCodeLensProvider implements CodeLensProvider {
private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined {
let valid = false;
let range: Range | undefined;
switch (languageLocation.location) {
case CodeLensLocation.All:
case CodeLensLocation.DocumentAndContainers:
switch (symbol.kind) {
case SymbolKind.File:
valid = true;
// Adjust the range to be the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
break;
case SymbolKind.Package:
case SymbolKind.Module:
// Adjust the range to be the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
valid = true;
break;
case SymbolKind.Namespace:
case SymbolKind.Class:
case SymbolKind.Interface:
valid = true;
break;
case SymbolKind.Constructor:
case SymbolKind.Method:
case SymbolKind.Function:
case SymbolKind.Property:
case SymbolKind.Enum:
valid = languageLocation.location === CodeLensLocation.All;
break;
switch (symbol.kind) {
case SymbolKind.File:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
else if (languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
if (valid) {
// Adjust the range to be for the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
break;
case CodeLensLocation.Custom:
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
case SymbolKind.Package:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
else if (languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
if (valid) {
switch (symbol.kind) {
case SymbolKind.File:
// Adjust the range to be the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
break;
case SymbolKind.Package:
case SymbolKind.Module:
// Adjust the range to be the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
break;
// Adjust the range to be for the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) {
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
}
}
break;
case SymbolKind.Class:
case SymbolKind.Interface:
case SymbolKind.Module:
case SymbolKind.Namespace:
case SymbolKind.Struct:
if (languageLocation.locations.includes(CodeLensLocations.Containers)) {
valid = true;
}
break;
case SymbolKind.Constructor:
case SymbolKind.Enum:
case SymbolKind.Function:
case SymbolKind.Method:
case SymbolKind.Property:
if (languageLocation.locations.includes(CodeLensLocations.Blocks)) {
valid = true;
}
break;
}
if (!valid && languageLocation.locations.includes(CodeLensLocations.Custom)) {
valid = !!(languageLocation.customSymbols || []).find(_ => _.toLowerCase() === SymbolKind[symbol.kind].toLowerCase());
}
return valid ? range || symbol.location.range : undefined;
@ -302,7 +307,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
_applyBlameAnnotateCommand<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;

+ 10
- 10
src/gitService.ts Переглянути файл

@ -2,9 +2,9 @@
import { Iterables, Objects } from './system';
import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode';
import { CommandContext, setCommandContext } from './commands';
import { CodeLensVisibility, IConfig } from './configuration';
import { IConfig } from './configuration';
import { DocumentSchemes, ExtensionKey } from './constants';
import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitLog, IGitStash, IGitStatus, setDefaultEncoding } from './git/git';
import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitDiffLine, IGitLog, IGitStash, IGitStatus, setDefaultEncoding } from './git/git';
import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { GitCodeLensProvider } from './gitCodeLensProvider';
import { Logger } from './logger';
@ -15,6 +15,7 @@ import * as path from 'path';
export { GitUri, IGitCommitInfo };
export * from './git/models/models';
export * from './git/formatters/commit';
export { getNameFromRemoteResource, RemoteResource, RemoteProvider } from './git/remotes/provider';
export * from './git/gitContextTracker';
@ -139,7 +140,7 @@ export class GitService extends Disposable {
if (codeLensChanged) {
Logger.log('CodeLens config changed; resetting CodeLens provider');
if (cfg.codeLens.visibility === CodeLensVisibility.Auto && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (this._codeLensProvider) {
this._codeLensProvider.reset();
}
@ -154,7 +155,7 @@ export class GitService extends Disposable {
this._codeLensProvider = undefined;
}
setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.visibility !== CodeLensVisibility.Off && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled));
setCommandContext(CommandContext.CanToggleCodeLens, cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled);
}
if (advancedChanged) {
@ -644,13 +645,13 @@ export class GitService extends Disposable {
}
}
async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<[string | undefined, string | undefined] | undefined> {
async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<[IGitDiffLine | undefined, IGitDiffLine | undefined]> {
try {
const diff = await this.getDiffForFile(uri, sha1, sha2);
if (diff === undefined) return undefined;
if (diff === undefined) return [undefined, undefined];
const chunk = diff.chunks.find(_ => _.currentStart <= line && _.currentEnd >= line);
if (chunk === undefined) return undefined;
if (chunk === undefined) return [undefined, undefined];
// Search for the line (skipping deleted lines -- since they don't currently exist in the editor)
// Keep track of the deleted lines for the original version
@ -675,7 +676,7 @@ export class GitService extends Disposable {
];
}
catch (ex) {
return undefined;
return [undefined, undefined];
}
}
@ -1008,8 +1009,7 @@ export class GitService extends Disposable {
}
toggleCodeLens(editor: TextEditor) {
if (this.config.codeLens.visibility === CodeLensVisibility.Off ||
(!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return;
if (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled) return;
Logger.log(`toggleCodeLens()`);
if (this._codeLensProviderDisposable) {

+ 1
- 2
src/quickPicks/common.ts Переглянути файл

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

+ 4
- 0
src/system/function.ts Переглянути файл

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

+ 6
- 0
src/system/object.ts Переглянути файл

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

+ 76
- 0
src/system/string.ts Переглянути файл

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

+ 2
- 2
tsconfig.json Переглянути файл

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

+ 1
- 1
tslint.json Переглянути файл

@ -39,7 +39,7 @@
"ignore-properties"
],
"no-internal-module": true,
"no-invalid-template-strings": true,
// "no-invalid-template-strings": true,
"no-irregular-whitespace": true,
"no-reference": true,
"no-string-throw": true,

Завантаження…
Відмінити
Зберегти