Преглед на файлове

Reworks explorers (wip)

Closes #354 - Adds line history explorer (wip)
Closes #456 - Adds repository status to the repository nodes
main
Eric Amodio преди 6 години
родител
ревизия
2e4cbcad10
променени са 65 файла, в които са добавени 2599 реда и са изтрити 2476 реда
  1. +40
    -6
      README.md
  2. +5
    -0
      images/dark/icon-repo-blue.svg
  3. +5
    -0
      images/light/icon-repo-blue.svg
  4. +155
    -50
      package.json
  5. +3
    -9
      src/commands.ts
  6. +2
    -1
      src/commands/common.ts
  7. +1
    -1
      src/commands/diffBranchWithBranch.ts
  8. +2
    -2
      src/commands/diffDirectory.ts
  9. +33
    -0
      src/commands/showExplorer.ts
  10. +0
    -13
      src/commands/showGitExplorer.ts
  11. +0
    -13
      src/commands/showHistoryExplorer.ts
  12. +0
    -13
      src/commands/showResultsExplorer.ts
  13. +2
    -1
      src/configuration.ts
  14. +2
    -1
      src/constants.ts
  15. +38
    -15
      src/container.ts
  16. +30
    -378
      src/extension.ts
  17. +6
    -6
      src/git/models/repository.ts
  18. +2
    -2
      src/quickpicks/commonQuickPicks.ts
  19. +8
    -4
      src/ui/config.ts
  20. +563
    -345
      src/ui/settings/index.html
  21. +116
    -36
      src/views/explorer.ts
  22. +7
    -8
      src/views/explorerCommands.ts
  23. +80
    -0
      src/views/fileHistoryExplorer.ts
  24. +29
    -200
      src/views/gitExplorer.ts
  25. +0
    -251
      src/views/historyExplorer.ts
  26. +80
    -0
      src/views/lineHistoryExplorer.ts
  27. +7
    -7
      src/views/nodes.ts
  28. +108
    -0
      src/views/nodes/activeFileHistoryNode.ts
  29. +116
    -0
      src/views/nodes/activeLineHistoryNode.ts
  30. +0
    -97
      src/views/nodes/activeRepositoryNode.ts
  31. +24
    -12
      src/views/nodes/branchNode.ts
  32. +28
    -5
      src/views/nodes/branchOrTagFolderNode.ts
  33. +8
    -18
      src/views/nodes/branchesNode.ts
  34. +11
    -9
      src/views/nodes/commitFileNode.ts
  35. +2
    -1
      src/views/nodes/commitNode.ts
  36. +0
    -38
      src/views/nodes/commitsNode.ts
  37. +0
    -74
      src/views/nodes/commitsResultsNode.ts
  38. +94
    -0
      src/views/nodes/common.ts
  39. +0
    -59
      src/views/nodes/comparisonResultsNode.ts
  40. +77
    -111
      src/views/nodes/explorerNode.ts
  41. +23
    -24
      src/views/nodes/fileHistoryNode.ts
  42. +2
    -1
      src/views/nodes/folderNode.ts
  43. +0
    -35
      src/views/nodes/historyNode.ts
  44. +131
    -0
      src/views/nodes/lineHistoryNode.ts
  45. +13
    -1
      src/views/nodes/remoteNode.ts
  46. +5
    -4
      src/views/nodes/remotesNode.ts
  47. +109
    -23
      src/views/nodes/repositoriesNode.ts
  48. +157
    -54
      src/views/nodes/repositoryNode.ts
  49. +1
    -1
      src/views/nodes/resultsCommitNode.ts
  50. +68
    -0
      src/views/nodes/resultsCommitsNode.ts
  51. +84
    -0
      src/views/nodes/resultsComparisonNode.ts
  52. +64
    -0
      src/views/nodes/resultsNode.ts
  53. +2
    -1
      src/views/nodes/stashFileNode.ts
  54. +7
    -1
      src/views/nodes/stashNode.ts
  55. +6
    -4
      src/views/nodes/stashesNode.ts
  56. +2
    -1
      src/views/nodes/statusFileCommitsNode.ts
  57. +9
    -2
      src/views/nodes/statusFileNode.ts
  58. +40
    -98
      src/views/nodes/statusFilesNode.ts
  59. +2
    -1
      src/views/nodes/statusFilesResultsNode.ts
  60. +0
    -180
      src/views/nodes/statusNode.ts
  61. +36
    -24
      src/views/nodes/statusUpstreamNode.ts
  62. +17
    -6
      src/views/nodes/tagNode.ts
  63. +6
    -5
      src/views/nodes/tagsNode.ts
  64. +104
    -197
      src/views/resultsExplorer.ts

+ 40
- 6
README.md Целия файл

@ -80,6 +80,7 @@ Here are just some of the features that GitLens provides,
- a [_GitLens_ explorer](#gitlens-explorer 'Jump to the GitLens explorer') to navigate and explore repositories
- a [_GitLens File History_ explorer](#gitlens-file-history-explorer 'Jump to the GitLens File History explorer') to navigate and explore file histories
- a [_GitLens Line History_ explorer](#gitlens-line-history-explorer 'Jump to the GitLens Line History explorer') to navigate and explore file line histories
- an on-demand [_GitLens Results_ explorer](#gitlens-results-explorer 'Jump to the GitLens Results explorer') to navigate and explore commit searches, visualize comparisons between branches, tags, commits, and more
- authorship [code lens](#code-lens 'Jump to the Code Lens') showing the most recent commit and # of authors to the top of files and/or on code blocks
- an unobtrusive [current line blame](#current-line-blame 'Jump to the Current Line Blame') annotation at the end of the line
@ -233,10 +234,31 @@ A [customizable](#gitlens-file-history-explorer-settings 'Jump to the GitLens Fi
- A toolbar provides a _Refresh_ command
- A context menu provides a _Follow Renames_ or _Don't Follow Renames_ command
The file history view provides the revision history of the current file, which has the following features,
The file history explorer provides the revision history of the current file, which has the following features,
- Automatically updates to track the current editor
- A context menu provides _Open File_, _Open File in Remote_ (if available), and _Refresh_ commands
- A context menu provides _Open File_, _Open File in Remote_ (if available), _Copy Remote File Url to Clipboard_ (if available), and _Refresh_ commands
- An inline toolbar provides an _Open File_ command
- Context menus for each revision (commit) provides
- _Open Changes_, _Open Changes with Working File_, _Open File_, _Open Revision_, _Open File in Remote_ (if available), _Open Revision in Remote_ (if available), _Apply Changes_, _Compare with Selected_ (when available), _Select for Compare_, and _Show Commit File Details_ commands
---
### GitLens Line History Explorer
<p align="center">
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/ss-gitlens-line-history-explorer.png" alt="GitLens Line History Explorer" />
</p>
A [customizable](#gitlens-line-history-explorer-settings 'Jump to the GitLens Line History Explorer settings') explorer to visualize the revision history of the selected lines of current file.
- A toolbar provides a _Refresh_ command
- A context menu provides a _Follow Renames_ or _Don't Follow Renames_ command
The line history explorer provides the revision history of the selected lines in the current file, which has the following features,
- Automatically updates to track the selection of the current editor
- A context menu provides _Open File_, _Open File in Remote_ (if available), _Copy Remote File Url to Clipboard_ (if available), and _Refresh_ commands
- An inline toolbar provides an _Open File_ command
- Context menus for each revision (commit) provides
- _Open Changes_, _Open Changes with Working File_, _Open File_, _Open Revision_, _Open File in Remote_ (if available), _Open Revision in Remote_ (if available), _Apply Changes_, _Compare with Selected_ (when available), _Select for Compare_, and _Show Commit File Details_ commands
@ -681,10 +703,21 @@ See also [Explorer Settings](#explorer-settings 'Jump to the Explorer settings')
See also [Explorer Settings](#explorer-settings 'Jump to the Explorer settings')
| Name | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.historyExplorer.enabled` | Specifies whether to show the current file history undocked in a _GitLens File History_ explorer |
| `gitlens.historyExplorer.location` | Specifies where to show the _GitLens File History_ explorer<br />`gitlens` - adds to the GitLens view<br />`explorer` - adds to the Explorer view<br />`scm` - adds to the Source Control view |
| Name | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.fileHistoryExplorer.avatars` | Specifies whether to show avatar images instead of status icons in the `GitLens File History` explorer |
| `gitlens.fileHistoryExplorer.enabled` | Specifies whether to show the _GitLens File History_ explorer |
| `gitlens.fileHistoryExplorer.location` | Specifies where to show the _GitLens File History_ explorer<br />`gitlens` - adds to the GitLens view<br />`explorer` - adds to the Explorer view<br />`scm` - adds to the Source Control view |
### GitLens Line History Explorer Settings
See also [Explorer Settings](#explorer-settings 'Jump to the Explorer settings')
| Name | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.fileHistoryExplorer.avatars` | Specifies whether to show avatar images instead of status icons in the `GitLens Line History` explorer |
| `gitlens.lineHistoryExplorer.enabled` | Specifies whether to show the _GitLens Line History_ explorer |
| `gitlens.lineHistoryExplorer.location` | Specifies where to show the _GitLens Line History_ explorer<br />`gitlens` - adds to the GitLens view<br />`explorer` - adds to the Explorer view<br />`scm` - adds to the Source Control view |
### GitLens Results Explorer Settings
@ -704,6 +737,7 @@ See also [Explorer Settings](#explorer-settings 'Jump to the Explorer settings')
| `gitlens.explorers.avatars` | Specifies whether to show avatar images instead of commit (or status) icons in the _GitLens_ and _GitLens Results_ explorers |
| `gitlens.explorers.commitFileFormat` | Specifies the format of a committed file in the _GitLens_ and _GitLens Results_ explorers<br />Available tokens<br /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full file path |
| `gitlens.explorers.commitFormat` | Specifies the format of committed changes in the _GitLens_ and _GitLens Results_ explorers<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${agoOrDate} - commit date specified by `gitlens.defaultDateStyle`<br /> ${authorAgo} - commit author, relative commit date<br /> ${authorAgoOrDate} - commit author, commit date specified by `gitlens.defaultDateStyle`<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |
| `gitlens.explorers.defaultItemLimit` | Specifies the default number of items to show in an explorer list. Use 0 to specify no limit |
| `gitlens.explorers.stashFileFormat` | Specifies the format of a stashed file in the _GitLens_ and _GitLens Results_ explorers<br />Available tokens<br /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full file path |
| `gitlens.explorers.stashFormat` | Specifies the format of stashed changes in the _GitLens_ and _GitLens Results_ explorers<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${agoOrDate} - commit date specified by `gitlens.defaultDateStyle`<br /> ${authorAgo} - commit author, relative commit date<br /> ${authorAgoOrDate} - commit author, commit date specified by `gitlens.defaultDateStyle`<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |
| `gitlens.explorers.statusFileFormat` | Specifies the format of the status of a working or committed file in the _GitLens_ and _GitLens Results_ explorers<br />Available tokens<br /> ${directory} - directory name<br /> ${file} - file name<br /> ${filePath} - formatted file name and path<br /> ${path} - full file path<br />${working} - optional indicator if the file is uncommitted |

+ 5
- 0
images/dark/icon-repo-blue.svg Целия файл

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" />
<ellipse fill="#0366d6" stroke="#C5C5C5" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" />
</svg>

+ 5
- 0
images/light/icon-repo-blue.svg Целия файл

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" />
<ellipse fill="#0366d6" stroke="#424242" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" />
</svg>

+ 155
- 50
package.json Целия файл

@ -455,6 +455,12 @@
"description": "Specifies the format of committed changes in the `GitLens` and `GitLens Results` explorers\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.defaultDateFormat`)\\n ${agoOrDate} - commit date specified by `gitlens.defaultDateStyle`\n ${authorAgo} - commit author, relative commit date\n ${authorAgoOrDate} - commit author, commit date specified by `gitlens.defaultDateStyle`\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window"
},
"gitlens.explorers.defaultItemLimit": {
"type": "number",
"default": 10,
"description": "Specifies the default number of items to show in an explorer list. Use 0 to specify no limit",
"scope": "window"
},
"gitlens.explorers.stashFileFormat": {
"type": "string",
"default": "${filePath}",
@ -587,19 +593,19 @@
"description": "Specifies how the gutter heatmap annotations will be toggled",
"scope": "window"
},
"gitlens.historyExplorer.avatars": {
"gitlens.fileHistoryExplorer.avatars": {
"type": "boolean",
"default": true,
"description": "Specifies whether to show avatar images instead of status icons in the `GitLens File History` explorer",
"scope": "window"
},
"gitlens.historyExplorer.enabled": {
"gitlens.fileHistoryExplorer.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether to show the current file history undocked in a `GitLens File History` explorer",
"description": "Specifies whether to show the `GitLens File History` explorer",
"scope": "window"
},
"gitlens.historyExplorer.location": {
"gitlens.fileHistoryExplorer.location": {
"type": "string",
"default": "gitlens",
"enum": [
@ -713,6 +719,34 @@
"description": "Specifies the keymap to use for GitLens shortcut keys",
"scope": "window"
},
"gitlens.lineHistoryExplorer.avatars": {
"type": "boolean",
"default": true,
"description": "Specifies whether to show avatar images instead of status icons in the `GitLens Line History` explorer",
"scope": "window"
},
"gitlens.lineHistoryExplorer.enabled": {
"type": "boolean",
"default": true,
"description": "Specifies whether to show the `GitLens Line History` explorer",
"scope": "window"
},
"gitlens.lineHistoryExplorer.location": {
"type": "string",
"default": "gitlens",
"enum": [
"gitlens",
"explorer",
"scm"
],
"enumDescriptions": [
"Adds to the GitLens view",
"Adds to the Explorer view",
"Adds to the Source Control view"
],
"description": "Specifies where to show the `GitLens Line History` explorer",
"scope": "window"
},
"gitlens.menus": {
"anyOf": [
{
@ -1487,11 +1521,16 @@
"category": "GitLens"
},
{
"command": "gitlens.showHistoryExplorer",
"command": "gitlens.showFileHistoryExplorer",
"title": "Show File History Explorer",
"category": "GitLens"
},
{
"command": "gitlens.showLineHistoryExplorer",
"title": "Show Line History Explorer",
"category": "GitLens"
},
{
"command": "gitlens.showResultsExplorer",
"title": "Show Results Explorer",
"category": "GitLens"
@ -2008,7 +2047,7 @@
"category": "GitLens"
},
{
"command": "gitlens.historyExplorer.refresh",
"command": "gitlens.fileHistoryExplorer.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": {
@ -2017,30 +2056,45 @@
}
},
{
"command": "gitlens.historyExplorer.refreshNode",
"command": "gitlens.fileHistoryExplorer.refreshNode",
"title": "Refresh",
"category": "GitLens"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOn",
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOn",
"title": "Follow Renames",
"category": "GitLens"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOff",
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOff",
"title": "Don't Follow Renames",
"category": "GitLens"
},
{
"command": "gitlens.resultsExplorer.clearResultsNode",
"title": "Clear Results",
"command": "gitlens.lineHistoryExplorer.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-close-small.svg",
"light": "images/light/icon-close-small.svg"
"dark": "images/dark/icon-refresh.svg",
"light": "images/light/icon-refresh.svg"
}
},
{
"command": "gitlens.lineHistoryExplorer.refreshNode",
"title": "Refresh",
"category": "GitLens"
},
{
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOn",
"title": "Follow Renames",
"category": "GitLens"
},
{
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOff",
"title": "Don't Follow Renames",
"category": "GitLens"
},
{
"command": "gitlens.resultsExplorer.close",
"title": "Close",
"category": "GitLens",
@ -2050,6 +2104,15 @@
}
},
{
"command": "gitlens.resultsExplorer.dismissNode",
"title": "Dismiss",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-close-small.svg",
"light": "images/light/icon-close-small.svg"
}
},
{
"command": "gitlens.resultsExplorer.refresh",
"title": "Refresh",
"category": "GitLens",
@ -2113,12 +2176,13 @@
"when": "gitlens:enabled && gitlens:gitExplorer"
},
{
"command": "gitlens.showHistoryExplorer",
"when": "gitlens:enabled && gitlens:historyExplorer"
"command": "gitlens.showFileHistoryExplorer",
"when": "gitlens:enabled && gitlens:fileHistoryExplorer"
},
{
"command": "gitlens.showHistoryExplorer",
"when": "gitlens:enabled && !gitlens:historyExplorer && gitlens:gitExplorer"
"command": "gitlens.showLineHistoryExplorer",
"title": "Show Line History Explorer",
"when": "gitlens:enabled && gitlens:lineHistoryExplorer"
},
{
"command": "gitlens.showResultsExplorer",
@ -2489,23 +2553,35 @@
"when": "false"
},
{
"command": "gitlens.historyExplorer.refresh",
"command": "gitlens.fileHistoryExplorer.refresh",
"when": "false"
},
{
"command": "gitlens.fileHistoryExplorer.refreshNode",
"when": "false"
},
{
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOn",
"when": "false"
},
{
"command": "gitlens.historyExplorer.refreshNode",
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOff",
"when": "false"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOn",
"command": "gitlens.lineHistoryExplorer.refresh",
"when": "false"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOff",
"command": "gitlens.lineHistoryExplorer.refreshNode",
"when": "false"
},
{
"command": "gitlens.resultsExplorer.clearResultsNode",
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOn",
"when": "false"
},
{
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOff",
"when": "false"
},
{
@ -2513,6 +2589,10 @@
"when": "false"
},
{
"command": "gitlens.resultsExplorer.dismissNode",
"when": "false"
},
{
"command": "gitlens.resultsExplorer.refresh",
"when": "false"
},
@ -2807,18 +2887,33 @@
"group": "2_gitlens"
},
{
"command": "gitlens.historyExplorer.refresh",
"when": "view =~ /^gitlens.historyExplorer:/",
"command": "gitlens.fileHistoryExplorer.refresh",
"when": "view =~ /^gitlens.fileHistoryExplorer:/",
"group": "navigation@1"
},
{
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOn",
"when": "view =~ /^gitlens.fileHistoryExplorer:/ && !config.gitlens.advanced.fileHistoryFollowsRenames",
"group": "1_gitlens"
},
{
"command": "gitlens.fileHistoryExplorer.setRenameFollowingOff",
"when": "view =~ /^gitlens.fileHistoryExplorer:/ && config.gitlens.advanced.fileHistoryFollowsRenames",
"group": "1_gitlens"
},
{
"command": "gitlens.lineHistoryExplorer.refresh",
"when": "view =~ /^gitlens.lineHistoryExplorer:/",
"group": "navigation@1"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOn",
"when": "view =~ /^gitlens.historyExplorer:/ && !config.gitlens.advanced.fileHistoryFollowsRenames",
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOn",
"when": "view =~ /^gitlens.lineHistoryExplorer:/ && !config.gitlens.advanced.fileHistoryFollowsRenames",
"group": "1_gitlens"
},
{
"command": "gitlens.historyExplorer.setRenameFollowingOff",
"when": "view =~ /^gitlens.historyExplorer:/ && config.gitlens.advanced.fileHistoryFollowsRenames",
"command": "gitlens.lineHistoryExplorer.setRenameFollowingOff",
"when": "view =~ /^gitlens.lineHistoryExplorer:/ && config.gitlens.advanced.fileHistoryFollowsRenames",
"group": "1_gitlens"
},
{
@ -3089,16 +3184,6 @@
"group": "1_gitlens@1"
},
{
"command": "gitlens.openRepoInRemote",
"when": "viewItem == gitlens:status && gitlens:hasRemotes",
"group": "1_gitlens@1"
},
{
"command": "gitlens.explorers.closeRepository",
"when": "viewItem == gitlens:status",
"group": "8_gitlens@1"
},
{
"command": "gitlens.openBranchesInRemote",
"when": "viewItem == gitlens:remote",
"group": "1_gitlens@1"
@ -3134,19 +3219,19 @@
"group": "inline@1"
},
{
"command": "gitlens.resultsExplorer.clearResultsNode",
"command": "gitlens.resultsExplorer.dismissNode",
"when": "viewItem =~ /gitlens:results\\b(?!:(commits|files))/",
"group": "inline@2"
},
{
"command": "gitlens.resultsExplorer.clearResultsNode",
"command": "gitlens.resultsExplorer.dismissNode",
"when": "viewItem =~ /gitlens:results\\b(?!:(commits|files))/",
"group": "1_gitlens@1"
},
{
"command": "gitlens.resultsExplorer.swapComparision",
"when": "viewItem == gitlens:results:comparison",
"group": "1_gitlens@2"
"group": "2_gitlens@1"
},
{
"command": "gitlens.explorers.openDirectoryDiff",
@ -3184,8 +3269,13 @@
"group": "9_gitlens@1"
},
{
"command": "gitlens.historyExplorer.refreshNode",
"when": "view =~ /^gitlens.historyExplorer:/ && viewItem =~ /gitlens:(?!file\\b)/",
"command": "gitlens.fileHistoryExplorer.refreshNode",
"when": "view =~ /^gitlens.fileHistoryExplorer:/ && viewItem =~ /gitlens:(?!file\\b)/",
"group": "9_gitlens@1"
},
{
"command": "gitlens.lineHistoryExplorer.refreshNode",
"when": "view =~ /^gitlens.lineHistoryExplorer:/ && viewItem =~ /gitlens:(?!file\\b)/",
"group": "9_gitlens@1"
}
]
@ -3390,13 +3480,18 @@
"gitlens": [
{
"id": "gitlens.gitExplorer:gitlens",
"name": "Explorer",
"name": "Repositories",
"when": "gitlens:enabled && gitlens:gitExplorer == gitlens"
},
{
"id": "gitlens.historyExplorer:gitlens",
"id": "gitlens.fileHistoryExplorer:gitlens",
"name": "File History",
"when": "gitlens:enabled && gitlens:historyExplorer == gitlens"
"when": "gitlens:enabled && gitlens:fileHistoryExplorer == gitlens"
},
{
"id": "gitlens.lineHistoryExplorer:gitlens",
"name": "Line History",
"when": "gitlens:enabled && gitlens:lineHistoryExplorer == gitlens"
},
{
"id": "gitlens.resultsExplorer:gitlens",
@ -3411,9 +3506,14 @@
"when": "gitlens:enabled && gitlens:gitExplorer == explorer"
},
{
"id": "gitlens.historyExplorer:explorer",
"id": "gitlens.fileHistoryExplorer:explorer",
"name": "GitLens File History",
"when": "gitlens:enabled && gitlens:historyExplorer == explorer"
"when": "gitlens:enabled && gitlens:fileHistoryExplorer == explorer"
},
{
"id": "gitlens.lineHistoryExplorer:explorer",
"name": "Line History",
"when": "gitlens:enabled && gitlens:lineHistoryExplorer == explorer"
},
{
"id": "gitlens.resultsExplorer:explorer",
@ -3428,9 +3528,14 @@
"when": "gitlens:enabled && gitlens:gitExplorer == scm"
},
{
"id": "gitlens.historyExplorer:scm",
"id": "gitlens.fileHistoryExplorer:scm",
"name": "GitLens File History",
"when": "gitlens:enabled && gitlens:historyExplorer == scm"
"when": "gitlens:enabled && gitlens:fileHistoryExplorer == scm"
},
{
"id": "gitlens.lineHistoryExplorer:scm",
"name": "Line History",
"when": "gitlens:enabled && gitlens:lineHistoryExplorer == scm"
},
{
"id": "gitlens.resultsExplorer:scm",

+ 3
- 9
src/commands.ts Целия файл

@ -26,8 +26,7 @@ import { OpenRepoInRemoteCommand } from './commands/openRepoInRemote';
import { OpenWorkingFileCommand } from './commands/openWorkingFile';
import { ResetSuppressedWarningsCommand } from './commands/resetSuppressedWarnings';
import { ShowCommitSearchCommand } from './commands/showCommitSearch';
import { ShowGitExplorerCommand } from './commands/showGitExplorer';
import { ShowHistoryExplorerCommand } from './commands/showHistoryExplorer';
import { ShowExplorerCommand } from './commands/showExplorer';
import { ShowLastQuickPickCommand } from './commands/showLastQuickPick';
import { ShowQuickBranchHistoryCommand } from './commands/showQuickBranchHistory';
import { ShowQuickCommitDetailsCommand } from './commands/showQuickCommitDetails';
@ -36,7 +35,6 @@ import { ShowQuickCurrentBranchHistoryCommand } from './commands/showQuickCurren
import { ShowQuickFileHistoryCommand } from './commands/showQuickFileHistory';
import { ShowQuickRepoStatusCommand } from './commands/showQuickRepoStatus';
import { ShowQuickStashListCommand } from './commands/showQuickStashList';
import { ShowResultsExplorerCommand } from './commands/showResultsExplorer';
import { StashApplyCommand } from './commands/stashApply';
import { StashDeleteCommand } from './commands/stashDelete';
import { StashSaveCommand } from './commands/stashSave';
@ -77,8 +75,7 @@ export * from './commands/openRepoInRemote';
export * from './commands/openWorkingFile';
export * from './commands/resetSuppressedWarnings';
export * from './commands/showCommitSearch';
export * from './commands/showGitExplorer';
export * from './commands/showHistoryExplorer';
export * from './commands/showExplorer';
export * from './commands/showLastQuickPick';
export * from './commands/showQuickBranchHistory';
export * from './commands/showQuickCommitDetails';
@ -87,7 +84,6 @@ export * from './commands/showQuickCurrentBranchHistory';
export * from './commands/showQuickFileHistory';
export * from './commands/showQuickRepoStatus';
export * from './commands/showQuickStashList';
export * from './commands/showResultsExplorer';
export * from './commands/stashApply';
export * from './commands/stashDelete';
export * from './commands/stashSave';
@ -128,8 +124,7 @@ export function configureCommands(): void {
Container.context.subscriptions.push(new OpenWorkingFileCommand());
Container.context.subscriptions.push(new ResetSuppressedWarningsCommand());
Container.context.subscriptions.push(new ShowCommitSearchCommand());
Container.context.subscriptions.push(new ShowGitExplorerCommand());
Container.context.subscriptions.push(new ShowHistoryExplorerCommand());
Container.context.subscriptions.push(new ShowExplorerCommand());
Container.context.subscriptions.push(new ShowLastQuickPickCommand());
Container.context.subscriptions.push(new ShowQuickBranchHistoryCommand());
Container.context.subscriptions.push(new ShowQuickCommitDetailsCommand());
@ -138,7 +133,6 @@ export function configureCommands(): void {
Container.context.subscriptions.push(new ShowQuickFileHistoryCommand());
Container.context.subscriptions.push(new ShowQuickRepoStatusCommand());
Container.context.subscriptions.push(new ShowQuickStashListCommand());
Container.context.subscriptions.push(new ShowResultsExplorerCommand());
Container.context.subscriptions.push(new StashApplyCommand());
Container.context.subscriptions.push(new StashDeleteCommand());
Container.context.subscriptions.push(new StashSaveCommand());

+ 2
- 1
src/commands/common.ts Целия файл

@ -56,7 +56,8 @@ export enum Commands {
ResetSuppressedWarnings = 'gitlens.resetSuppressedWarnings',
ShowCommitSearch = 'gitlens.showCommitSearch',
ShowGitExplorer = 'gitlens.showGitExplorer',
ShowHistoryExplorer = 'gitlens.showHistoryExplorer',
ShowFileHistoryExplorer = 'gitlens.showFileHistoryExplorer',
ShowLineHistoryExplorer = 'gitlens.showLineHistoryExplorer',
ShowLastQuickPick = 'gitlens.showLastQuickPick',
ShowQuickCommitDetails = 'gitlens.showQuickCommitDetails',
ShowQuickCommitFileDetails = 'gitlens.showQuickCommitFileDetails',

+ 1
- 1
src/commands/diffBranchWithBranch.ts Целия файл

@ -80,7 +80,7 @@ export class DiffBranchWithBranchCommand extends ActiveEditorCommand {
if (args.ref1 === undefined) return undefined;
}
await Container.resultsExplorer.showComparisonInResults(repoPath, args.ref1, args.ref2);
await Container.resultsExplorer.addComparison(repoPath, args.ref1, args.ref2);
return undefined;
}

+ 2
- 2
src/commands/diffDirectory.ts Целия файл

@ -5,7 +5,7 @@ import { Container } from '../container';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { BranchesAndTagsQuickPick, CommandQuickPickItem } from '../quickpicks';
import { ComparisonResultsNode } from '../views/nodes';
import { ResultsComparisonNode } from '../views/nodes';
import {
ActiveEditorCommand,
CommandContext,
@ -38,7 +38,7 @@ export class DiffDirectoryCommand extends ActiveEditorCommand {
break;
case Commands.ExplorersOpenDirectoryDiff:
if (context.type === 'view' && context.node instanceof ComparisonResultsNode) {
if (context.type === 'view' && context.node instanceof ResultsComparisonNode) {
args.ref1 = context.node.ref1.ref;
args.ref2 = context.node.ref2.ref;
}

+ 33
- 0
src/commands/showExplorer.ts Целия файл

@ -0,0 +1,33 @@
'use strict';
import { Container } from '../container';
import { Command, CommandContext, Commands } from './common';
export class ShowExplorerCommand extends Command {
constructor() {
super([
Commands.ShowGitExplorer,
Commands.ShowFileHistoryExplorer,
Commands.ShowLineHistoryExplorer,
Commands.ShowResultsExplorer
]);
}
protected async preExecute(context: CommandContext): Promise<any> {
return this.execute(context.command as Commands);
}
execute(command: Commands) {
switch (command) {
case Commands.ShowGitExplorer:
return Container.gitExplorer.show();
case Commands.ShowFileHistoryExplorer:
return Container.fileHistoryExplorer.show();
case Commands.ShowLineHistoryExplorer:
return Container.lineHistoryExplorer.show();
case Commands.ShowResultsExplorer:
return Container.resultsExplorer.show();
}
return undefined;
}
}

+ 0
- 13
src/commands/showGitExplorer.ts Целия файл

@ -1,13 +0,0 @@
'use strict';
import { Container } from '../container';
import { Command, Commands } from './common';
export class ShowGitExplorerCommand extends Command {
constructor() {
super(Commands.ShowGitExplorer);
}
execute() {
return Container.gitExplorer.show();
}
}

+ 0
- 13
src/commands/showHistoryExplorer.ts Целия файл

@ -1,13 +0,0 @@
'use strict';
import { Container } from '../container';
import { Command, Commands } from './common';
export class ShowHistoryExplorerCommand extends Command {
constructor() {
super(Commands.ShowHistoryExplorer);
}
execute() {
return Container.historyExplorer.show();
}
}

+ 0
- 13
src/commands/showResultsExplorer.ts Целия файл

@ -1,13 +0,0 @@
'use strict';
import { Container } from '../container';
import { Command, Commands } from './common';
export class ShowResultsExplorerCommand extends Command {
constructor() {
super(Commands.ShowResultsExplorer);
}
execute() {
return Container.resultsExplorer.show();
}
}

+ 2
- 1
src/configuration.ts Целия файл

@ -43,8 +43,9 @@ export class Configuration {
`gitlens.${this.name('codeLens').value}`,
`gitlens.${this.name('currentLine').value}`,
`gitlens.${this.name('gitExplorer').value}`,
`gitlens.${this.name('historyExplorer').value}`,
`gitlens.${this.name('fileHistoryExplorer').value}`,
`gitlens.${this.name('hovers').value}`,
`gitlens.${this.name('lineHistoryExplorer').value}`,
`gitlens.${this.name('statusBar').value}`
];
}

+ 2
- 1
src/constants.ts Целия файл

@ -33,7 +33,8 @@ export enum CommandContext {
GitExplorer = 'gitlens:gitExplorer',
GitExplorerAutoRefresh = 'gitlens:gitExplorer:autoRefresh',
HasRemotes = 'gitlens:hasRemotes',
HistoryExplorer = 'gitlens:historyExplorer',
FileHistoryExplorer = 'gitlens:fileHistoryExplorer',
LineHistoryExplorer = 'gitlens:lineHistoryExplorer',
Key = 'gitlens:key',
KeyMap = 'gitlens:keymap',
ResultsExplorer = 'gitlens:resultsExplorer',

+ 38
- 15
src/container.ts Целия файл

@ -12,8 +12,9 @@ import { StatusBarController } from './statusbar/statusBarController';
import { GitDocumentTracker } from './trackers/gitDocumentTracker';
import { GitLineTracker } from './trackers/gitLineTracker';
import { ExplorerCommands } from './views/explorerCommands';
import { FileHistoryExplorer } from './views/fileHistoryExplorer';
import { GitExplorer } from './views/gitExplorer';
import { HistoryExplorer } from './views/historyExplorer';
import { LineHistoryExplorer } from './views/lineHistoryExplorer';
import { ResultsExplorer } from './views/resultsExplorer';
import { SettingsEditor } from './webviews/settingsEditor';
import { WelcomeEditor } from './webviews/welcomeEditor';
@ -52,15 +53,28 @@ export class Container {
});
}
if (config.historyExplorer.enabled) {
context.subscriptions.push((this._historyExplorer = new HistoryExplorer()));
if (config.fileHistoryExplorer.enabled) {
context.subscriptions.push((this._fileHistoryExplorer = new FileHistoryExplorer()));
}
else {
let disposable: Disposable;
disposable = configuration.onDidChange(e => {
if (configuration.changed(e, configuration.name('historyExplorer')('enabled').value)) {
if (configuration.changed(e, configuration.name('fileHistoryExplorer')('enabled').value)) {
disposable.dispose();
context.subscriptions.push((this._historyExplorer = new HistoryExplorer()));
context.subscriptions.push((this._fileHistoryExplorer = new FileHistoryExplorer()));
}
});
}
if (config.lineHistoryExplorer.enabled) {
context.subscriptions.push((this._lineHistoryExplorer = new LineHistoryExplorer()));
}
else {
let disposable: Disposable;
disposable = configuration.onDidChange(e => {
if (configuration.changed(e, configuration.name('lineHistoryExplorer')('enabled').value)) {
disposable.dispose();
context.subscriptions.push((this._lineHistoryExplorer = new LineHistoryExplorer()));
}
});
}
@ -99,6 +113,15 @@ export class Container {
return this._fileAnnotationController;
}
private static _fileHistoryExplorer: FileHistoryExplorer | undefined;
static get fileHistoryExplorer() {
if (this._fileHistoryExplorer === undefined) {
this._context.subscriptions.push((this._fileHistoryExplorer = new FileHistoryExplorer()));
}
return this._fileHistoryExplorer;
}
private static _git: GitService;
static get git() {
return this._git;
@ -109,15 +132,6 @@ export class Container {
return this._gitExplorer!;
}
private static _historyExplorer: HistoryExplorer | undefined;
static get historyExplorer() {
if (this._historyExplorer === undefined) {
this._context.subscriptions.push((this._historyExplorer = new HistoryExplorer()));
}
return this._historyExplorer;
}
private static _keyboard: Keyboard;
static get keyboard() {
return this._keyboard;
@ -128,6 +142,15 @@ export class Container {
return this._lineAnnotationController;
}
private static _lineHistoryExplorer: LineHistoryExplorer | undefined;
static get lineHistoryExplorer() {
if (this._lineHistoryExplorer === undefined) {
this._context.subscriptions.push((this._lineHistoryExplorer = new LineHistoryExplorer()));
}
return this._lineHistoryExplorer;
}
private static _lineHoverController: LineHoverController;
static get lineHovers() {
return this._lineHoverController;
@ -187,7 +210,7 @@ export class Container {
config.gitExplorer.enabled = mode.explorers;
}
if (mode.explorers != null) {
config.historyExplorer.enabled = mode.explorers;
config.fileHistoryExplorer.enabled = mode.explorers;
}
if (mode.hovers != null) {
config.hovers.enabled = mode.hovers;

+ 30
- 378
src/extension.ts Целия файл

@ -2,17 +2,7 @@
import { commands, ExtensionContext, extensions, window, workspace } from 'vscode';
import { Commands, configureCommands } from './commands';
import {
CodeLensLanguageScope,
CodeLensScopes,
configuration,
Configuration,
HighlightLocations,
IConfig,
IMenuConfig,
KeyMap,
OutputLevel
} from './configuration';
import { configuration, Configuration, IConfig } from './configuration';
import { CommandContext, extensionQualifiedId, GlobalState, GlyphChars, setCommandContext } from './constants';
import { Container } from './container';
import { GitService } from './git/gitService';
@ -41,6 +31,22 @@ export async function activate(context: ExtensionContext) {
Configuration.configure(context);
const cfg = configuration.get<IConfig>();
// Pretend we are enabled (until we know otherwise) and set the explorer contexts to reduce flashing on load
await Promise.all([
setCommandContext(CommandContext.Enabled, true),
setCommandContext(CommandContext.GitExplorer, cfg.gitExplorer.enabled ? cfg.gitExplorer.location : false),
setCommandContext(
CommandContext.FileHistoryExplorer,
cfg.fileHistoryExplorer.enabled ? cfg.fileHistoryExplorer.location : false
),
setCommandContext(
CommandContext.LineHistoryExplorer,
cfg.lineHistoryExplorer.enabled ? cfg.lineHistoryExplorer.location : false
)
]);
const previousVersion = context.globalState.get<string>(GlobalState.GitLensVersion);
await migrateSettings(context, previousVersion);
@ -60,7 +66,6 @@ export async function activate(context: ExtensionContext) {
return;
}
const cfg = configuration.get<IConfig>();
Container.initialize(context, cfg);
configureCommands();
@ -94,385 +99,32 @@ async function migrateSettings(context: ExtensionContext, previousVersion: strin
const previous = Versions.fromString(previousVersion);
try {
if (Versions.compare(previous, Versions.from(7, 5, 10)) !== 1) {
await configuration.migrate(
'annotations.file.gutter.gravatars',
configuration.name('blame')('avatars').value
);
await configuration.migrate(
'annotations.file.gutter.compact',
configuration.name('blame')('compact').value
);
await configuration.migrate(
'annotations.file.gutter.dateFormat',
configuration.name('blame')('dateFormat').value
);
await configuration.migrate('annotations.file.gutter.format', configuration.name('blame')('format').value);
await configuration.migrate(
'annotations.file.gutter.heatmap.enabled',
configuration.name('blame')('heatmap')('enabled').value
);
await configuration.migrate(
'annotations.file.gutter.heatmap.location',
configuration.name('blame')('heatmap')('location').value
);
await configuration.migrate(
'annotations.file.gutter.lineHighlight.enabled',
configuration.name('blame')('highlight')('enabled').value
);
await configuration.migrate(
'annotations.file.gutter.lineHighlight.locations',
configuration.name('blame')('highlight')('locations').value
);
await configuration.migrate(
'annotations.file.gutter.separateLines',
configuration.name('blame')('separateLines').value
);
await configuration.migrate('codeLens.locations', configuration.name('codeLens')('scopes').value);
await configuration.migrate<
{ customSymbols?: string[]; language: string | undefined; locations: CodeLensScopes[] }[],
CodeLensLanguageScope[]
>('codeLens.perLanguageLocations', configuration.name('codeLens')('scopesByLanguage').value, {
migrationFn: v => {
const scopes = v.map(ls => {
return {
language: ls.language,
scopes: ls.locations,
symbolScopes: ls.customSymbols
};
});
return scopes;
}
});
await configuration.migrate(
'codeLens.customLocationSymbols',
configuration.name('codeLens')('symbolScopes').value
);
await configuration.migrate(
'annotations.line.trailing.dateFormat',
configuration.name('currentLine')('dateFormat').value
);
await configuration.migrate('blame.line.enabled', configuration.name('currentLine')('enabled').value);
await configuration.migrate(
'annotations.line.trailing.format',
configuration.name('currentLine')('format').value
);
if (Versions.compare(previous, Versions.from(9, 0, 0)) !== 1) {
await configuration.migrate(
'annotations.file.gutter.hover.changes',
configuration.name('hovers')('annotations')('changes').value
'historyExplorer.avatars',
configuration.name('fileHistoryExplorer')('avatars').value
);
await configuration.migrate(
'annotations.file.gutter.hover.details',
configuration.name('hovers')('annotations')('details').value
'historyExplorer.enabled',
configuration.name('fileHistoryExplorer')('enabled').value
);
await configuration.migrate(
'annotations.file.gutter.hover.details',
configuration.name('hovers')('annotations')('enabled').value
);
await configuration.migrate<boolean, 'line' | 'annotation'>(
'annotations.file.gutter.hover.wholeLine',
configuration.name('hovers')('annotations')('over').value,
{ migrationFn: v => (v ? 'line' : 'annotation') }
'historyExplorer.location',
configuration.name('fileHistoryExplorer')('location').value
);
await configuration.migrate(
'annotations.line.trailing.hover.changes',
configuration.name('hovers')('currentLine')('changes').value
);
await configuration.migrate(
'annotations.line.trailing.hover.details',
configuration.name('hovers')('currentLine')('details').value
'historyExplorer.avatars',
configuration.name('lineHistoryExplorer')('avatars').value
);
await configuration.migrate(
'blame.line.enabled',
configuration.name('hovers')('currentLine')('enabled').value
'historyExplorer.enabled',
configuration.name('lineHistoryExplorer')('enabled').value
);
await configuration.migrate<boolean, 'line' | 'annotation'>(
'annotations.line.trailing.hover.wholeLine',
configuration.name('hovers')('currentLine')('over').value,
{ migrationFn: v => (v ? 'line' : 'annotation') }
);
await configuration.migrate('gitExplorer.gravatars', configuration.name('explorers')('avatars').value);
await configuration.migrate(
'gitExplorer.commitFileFormat',
configuration.name('explorers')('commitFileFormat').value
);
await configuration.migrate(
'gitExplorer.commitFormat',
configuration.name('explorers')('commitFormat').value
);
await configuration.migrate(
'gitExplorer.stashFileFormat',
configuration.name('explorers')('stashFileFormat').value
);
await configuration.migrate(
'gitExplorer.stashFormat',
configuration.name('explorers')('stashFormat').value
);
await configuration.migrate(
'gitExplorer.statusFileFormat',
configuration.name('explorers')('statusFileFormat').value
);
await configuration.migrate(
'recentChanges.file.lineHighlight.locations',
configuration.name('recentChanges')('highlight')('locations').value
);
}
if (Versions.compare(previous, Versions.from(8, 0, 0, 'beta2')) !== 1) {
await configuration.migrate<boolean, OutputLevel>('debug', configuration.name('outputLevel').value, {
migrationFn: v =>
v ? OutputLevel.Debug : configuration.get<OutputLevel>(configuration.name('outputLevel').value)
});
await configuration.migrate('debug', configuration.name('debug').value, { migrationFn: v => undefined });
}
if (Versions.compare(previous, Versions.from(8, 0, 0, 'rc')) !== 1) {
let section = configuration.name('blame')('highlight')('locations').value;
await configuration.migrate<('gutter' | 'line' | 'overviewRuler')[], HighlightLocations[]>(
section,
section,
{
migrationFn: v => {
const index = v.indexOf('overviewRuler');
if (index !== -1) {
v.splice(index, 1, 'overview' as 'overviewRuler');
}
return v as HighlightLocations[];
}
}
);
section = configuration.name('recentChanges')('highlight')('locations').value;
await configuration.migrate<('gutter' | 'line' | 'overviewRuler')[], HighlightLocations[]>(
section,
section,
{
migrationFn: v => {
const index = v.indexOf('overviewRuler');
if (index !== -1) {
v.splice(index, 1, 'overview' as 'overviewRuler');
}
return v as HighlightLocations[];
}
}
);
}
if (Versions.compare(previous, Versions.from(8, 0, 0)) !== 1) {
await configuration.migrateIfMissing(
'annotations.file.gutter.gravatars',
configuration.name('blame')('avatars').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.compact',
configuration.name('blame')('compact').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.dateFormat',
configuration.name('blame')('dateFormat').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.format',
configuration.name('blame')('format').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.heatmap.enabled',
configuration.name('blame')('heatmap')('enabled').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.heatmap.location',
configuration.name('blame')('heatmap')('location').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.lineHighlight.enabled',
configuration.name('blame')('highlight')('enabled').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.lineHighlight.locations',
configuration.name('blame')('highlight')('locations').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.separateLines',
configuration.name('blame')('separateLines').value
);
await configuration.migrateIfMissing('codeLens.locations', configuration.name('codeLens')('scopes').value);
await configuration.migrateIfMissing<
{ customSymbols?: string[]; language: string | undefined; locations: CodeLensScopes[] }[],
CodeLensLanguageScope[]
>('codeLens.perLanguageLocations', configuration.name('codeLens')('scopesByLanguage').value, {
migrationFn: v => {
const scopes = v.map(ls => {
return {
language: ls.language,
scopes: ls.locations,
symbolScopes: ls.customSymbols
};
});
return scopes;
}
});
await configuration.migrateIfMissing(
'codeLens.customLocationSymbols',
configuration.name('codeLens')('symbolScopes').value
);
await configuration.migrateIfMissing(
'annotations.line.trailing.dateFormat',
configuration.name('currentLine')('dateFormat').value
);
await configuration.migrateIfMissing(
'blame.line.enabled',
configuration.name('currentLine')('enabled').value
);
await configuration.migrateIfMissing(
'annotations.line.trailing.format',
configuration.name('currentLine')('format').value
'historyExplorer.location',
configuration.name('lineHistoryExplorer')('location').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.hover.changes',
configuration.name('hovers')('annotations')('changes').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.hover.details',
configuration.name('hovers')('annotations')('details').value
);
await configuration.migrateIfMissing(
'annotations.file.gutter.hover.details',
configuration.name('hovers')('annotations')('enabled').value
);
await configuration.migrateIfMissing<boolean, 'line' | 'annotation'>(
'annotations.file.gutter.hover.wholeLine',
configuration.name('hovers')('annotations')('over').value,
{ migrationFn: v => (v ? 'line' : 'annotation') }
);
await configuration.migrateIfMissing(
'annotations.line.trailing.hover.changes',
configuration.name('hovers')('currentLine')('changes').value
);
await configuration.migrateIfMissing(
'annotations.line.trailing.hover.details',
configuration.name('hovers')('currentLine')('details').value
);
await configuration.migrateIfMissing(
'blame.line.enabled',
configuration.name('hovers')('currentLine')('enabled').value
);
await configuration.migrateIfMissing<boolean, 'line' | 'annotation'>(
'annotations.line.trailing.hover.wholeLine',
configuration.name('hovers')('currentLine')('over').value,
{ migrationFn: v => (v ? 'line' : 'annotation') }
);
await configuration.migrateIfMissing(
'gitExplorer.gravatars',
configuration.name('explorers')('avatars').value
);
await configuration.migrateIfMissing(
'gitExplorer.commitFileFormat',
configuration.name('explorers')('commitFileFormat').value
);
await configuration.migrateIfMissing(
'gitExplorer.commitFormat',
configuration.name('explorers')('commitFormat').value
);
await configuration.migrateIfMissing(
'gitExplorer.stashFileFormat',
configuration.name('explorers')('stashFileFormat').value
);
await configuration.migrateIfMissing(
'gitExplorer.stashFormat',
configuration.name('explorers')('stashFormat').value
);
await configuration.migrateIfMissing(
'gitExplorer.statusFileFormat',
configuration.name('explorers')('statusFileFormat').value
);
await configuration.migrateIfMissing(
'recentChanges.file.lineHighlight.locations',
configuration.name('recentChanges')('highlight')('locations').value
);
}
if (Versions.compare(previous, Versions.from(8, 0, 2)) !== 1) {
const section = configuration.name('keymap').value;
await configuration.migrate<'standard' | 'chorded' | 'none', KeyMap>(section, section, {
fallbackValue: KeyMap.Alternate,
migrationFn: v => (v === 'standard' ? KeyMap.Alternate : (v as KeyMap))
});
}
if (Versions.compare(previous, Versions.from(8, 2, 4)) !== 1) {
await configuration.migrate<
{
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;
remote: boolean;
status: boolean;
};
editorTitleContext: {
blame: boolean;
fileDiff: boolean;
history: boolean;
remote: boolean;
};
},
IMenuConfig
>('advanced.menus', configuration.name('menus').value, {
migrationFn: m => {
return {
editor: {
blame: !!m.editorContext.blame,
clipboard: !!m.editorContext.copy,
compare: !!m.editorContext.lineDiff,
details: !!m.editorContext.details,
history: !!m.editorContext.history,
remote: !!m.editorContext.remote
},
editorGroup: {
blame: !!m.editorTitle.blame,
compare: !!m.editorTitle.fileDiff,
history: !!m.editorTitle.history,
remote: !!m.editorTitle.remote
},
editorTab: {
compare: !!m.editorTitleContext.fileDiff,
history: !!m.editorTitleContext.history,
remote: !!m.editorTitleContext.remote
},
explorer: {
compare: !!m.explorerContext.fileDiff,
history: !!m.explorerContext.history,
remote: !!m.explorerContext.remote
}
} as IMenuConfig;
}
});
}
}
catch (ex) {

+ 6
- 6
src/git/models/repository.ts Целия файл

@ -218,13 +218,13 @@ export class Repository implements Disposable {
}
}
// containsUri(uri: Uri) {
// if (uri instanceof GitUri) {
// uri = uri.repoPath !== undefined ? Uri.file(uri.repoPath) : uri.documentUri();
// }
containsUri(uri: Uri) {
if (uri instanceof GitUri) {
uri = uri.repoPath !== undefined ? Uri.file(uri.repoPath) : uri.documentUri();
}
// return this.folder === workspace.getWorkspaceFolder(uri);
// }
return this.folder === workspace.getWorkspaceFolder(uri);
}
getBranch(): Promise<GitBranch | undefined> {
if (this._branch === undefined) {

+ 2
- 2
src/quickpicks/commonQuickPicks.ts Целия файл

@ -247,7 +247,7 @@ export class ShowCommitInResultsQuickPickItem extends CommandQuickPickItem {
async execute(
options: TextDocumentShowOptions = { preserveFocus: false, preview: false }
): Promise<{} | undefined> {
await Container.resultsExplorer.showCommitInResults(this.commit);
await Container.resultsExplorer.addCommit(this.commit);
return undefined;
}
}
@ -267,7 +267,7 @@ export class ShowCommitsInResultsQuickPickItem extends CommandQuickPickItem {
async execute(
options: TextDocumentShowOptions = { preserveFocus: false, preview: false }
): Promise<{} | undefined> {
Container.resultsExplorer.showCommitsInResults(this.results, this.resultsLabel);
await Container.resultsExplorer.addSearchResults(this.results, this.resultsLabel);
return undefined;
}
}

+ 8
- 4
src/ui/config.ts Целия файл

@ -161,6 +161,7 @@ export interface IExplorersConfig {
commitFileFormat: string;
commitFormat: string;
// dateFormat: string | null;
defaultItemLimit: number;
stashFileFormat: string;
stashFormat: string;
@ -187,12 +188,14 @@ export interface IGitExplorerConfig {
showTrackingBranch: boolean;
}
export interface IHistoryExplorerConfig {
export interface IFileHistoryExplorerConfig {
avatars: boolean;
enabled: boolean;
location: 'explorer' | 'scm';
location: 'explorer' | 'gitlens' | 'scm';
}
export interface ILineHistoryExplorerConfig extends IFileHistoryExplorerConfig {}
export interface IMenuConfig {
editor:
| boolean
@ -239,7 +242,7 @@ export interface IModeConfig {
export interface IResultsExplorerConfig {
files: IExplorersFilesConfig;
location: 'explorer' | 'scm';
location: 'explorer' | 'gitlens' | 'scm';
}
export interface IRemotesConfig {
@ -306,7 +309,7 @@ export interface IConfig {
toggleMode: AnnotationsToggleMode;
};
historyExplorer: IHistoryExplorerConfig;
fileHistoryExplorer: IFileHistoryExplorerConfig;
hovers: {
annotations: {
@ -329,6 +332,7 @@ export interface IConfig {
insiders: boolean;
keymap: KeyMap;
lineHistoryExplorer: ILineHistoryExplorerConfig;
menus: boolean | IMenuConfig;
mode: {
active: string;

+ 563
- 345
src/ui/settings/index.html
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 116
- 36
src/views/explorer.ts Целия файл

@ -6,91 +6,171 @@ import {
EventEmitter,
TreeDataProvider,
TreeItem,
TreeView
TreeView,
TreeViewVisibilityChangeEvent,
window
} from 'vscode';
// import { configuration } from '../configuration';
// import { Container } from '../container';
import { configuration } from '../configuration';
import { Container } from '../container';
import { Logger } from '../logger';
import { RefreshNodeCommandArgs } from './explorerCommands';
import { ExplorerNode, RefreshReason } from './nodes';
import { FileHistoryExplorer } from './fileHistoryExplorer';
import { GitExplorer } from './gitExplorer';
import { LineHistoryExplorer } from './lineHistoryExplorer';
import { ExplorerNode } from './nodes';
import { isPageable } from './nodes/explorerNode';
import { ResultsExplorer } from './resultsExplorer';
export enum RefreshReason {
ActiveEditorChanged = 'active-editor-changed',
AutoRefreshChanged = 'auto-refresh-changed',
Command = 'command',
ConfigurationChanged = 'configuration',
NodeCommand = 'node-command',
RepoChanged = 'repo-changed',
ViewChanged = 'view-changed',
VisibleEditorsChanged = 'visible-editors-changed'
}
export type Explorer = GitExplorer | FileHistoryExplorer | LineHistoryExplorer | ResultsExplorer;
export abstract class ExplorerBase implements TreeDataProvider<ExplorerNode>, Disposable {
export abstract class ExplorerBase<TRoot extends ExplorerNode> implements TreeDataProvider<ExplorerNode>, Disposable {
protected _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> {
return this._onDidChangeTreeData.event;
}
private _onDidChangeVisibility = new EventEmitter<TreeViewVisibilityChangeEvent>();
public get onDidChangeVisibility(): Event<TreeViewVisibilityChangeEvent> {
return this._onDidChangeVisibility.event;
}
protected _disposable: Disposable | undefined;
protected _roots: ExplorerNode[] = [];
protected _root: TRoot | undefined;
protected _tree: TreeView<ExplorerNode> | undefined;
constructor() {
constructor(
public readonly id: string
) {
this.registerCommands();
Container.context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this));
setImmediate(() => this.onConfigurationChanged(configuration.initializingChangeEvent));
}
dispose() {
this._disposable && this._disposable.dispose();
}
abstract get id(): string;
getQualifiedCommand(command: string) {
return `${this.id}.${command}`;
}
protected abstract getRoot(): TRoot;
protected abstract registerCommands(): void;
protected abstract onConfigurationChanged(e: ConfigurationChangeEvent): void;
abstract getChildren(node?: ExplorerNode): Promise<ExplorerNode[]>;
getParent(element: ExplorerNode): ExplorerNode | undefined {
protected initialize(container?: string) {
if (this._disposable) {
this._disposable.dispose();
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>();
}
this._tree = window.createTreeView(`${this.id}${container ? `:${container}` : ''}`, {
treeDataProvider: this
});
this._disposable = Disposable.from(
this._tree,
this._tree.onDidChangeVisibility(this.onVisibilityChanged, this)
);
}
getChildren(node?: ExplorerNode): ExplorerNode[] | Promise<ExplorerNode[]> {
if (node !== undefined) return node.getChildren();
if (this._root === undefined) {
this._root = this.getRoot();
}
return this._root.getChildren();
}
getParent(): ExplorerNode | undefined {
return undefined;
}
abstract getTreeItem(node: ExplorerNode): Promise<TreeItem>;
protected getQualifiedCommand(command: string) {
return `gitlens.${this.id}.${command}`;
getTreeItem(node: ExplorerNode): TreeItem | Promise<TreeItem> {
return node.getTreeItem();
}
refresh(reason?: RefreshReason) {
protected onVisibilityChanged(e: TreeViewVisibilityChangeEvent) {
this._onDidChangeVisibility.fire(e);
}
get visible(): boolean {
return this._tree !== undefined ? this._tree.visible : false;
}
async refresh(reason?: RefreshReason) {
if (reason === undefined) {
reason = RefreshReason.Command;
}
Logger.log(`Explorer(${this.id}).refresh`, `reason='${reason}'`);
this._onDidChangeTreeData.fire();
if (this._root !== undefined) {
await this._root.refresh();
}
this.triggerNodeUpdate();
}
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
async refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
Logger.log(`Explorer(${this.id}).refreshNode(${(node as { id?: string }).id || ''})`);
if (args !== undefined && node.supportsPaging) {
node.maxCount = args.maxCount;
if (args !== undefined) {
if (isPageable(node)) {
if (args.maxCount === undefined || args.maxCount === 0) {
node.maxCount = args.maxCount;
}
else {
node.maxCount = (node.maxCount || args.maxCount) + args.maxCount;
}
}
}
node.refresh();
// Since a root node won't actually refresh, force everything
this.updateNode(node);
}
refreshNodes() {
Logger.log(`Explorer(${this.id}).refreshNodes`);
await node.refresh();
this._roots.forEach(n => n.refresh());
this._onDidChangeTreeData.fire();
this.triggerNodeUpdate(node);
}
async show() {
if (this._tree === undefined || this._roots === undefined || this._roots.length === 0) return;
async reveal(
node: ExplorerNode,
options?: {
select?: boolean | undefined;
focus?: boolean | undefined;
}
) {
if (this._tree === undefined || this._root === undefined) return;
try {
await this._tree.reveal(this._roots[0], { select: false });
await this._tree.reveal(node, options);
}
catch (ex) {
Logger.error(ex);
}
}
updateNode(node: ExplorerNode | undefined) {
Logger.log(`Explorer(${this.id}).updateNode`);
if (node !== undefined) {
node = this._roots.includes(node) ? undefined : node;
}
this._onDidChangeTreeData.fire(node);
async show() {
if (this._tree === undefined || this._root === undefined) return;
// This sucks -- have to get the first child to reveal the tree
const [child] = await this._root.getChildren();
return this.reveal(child, { select: false, focus: true });
}
triggerNodeUpdate(node?: ExplorerNode) {
// Since the root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(node !== undefined && node !== this._root ? node : undefined);
}
}

+ 7
- 8
src/views/explorerCommands.ts Целия файл

@ -29,7 +29,6 @@ import {
StashNode,
StatusFileCommitsNode,
StatusFileNode,
StatusNode,
StatusUpstreamNode,
TagNode
} from './nodes';
@ -111,8 +110,8 @@ export class ExplorerCommands implements Disposable {
}
}
private closeRepository(node: RepositoryNode | StatusNode) {
if (!(node instanceof RepositoryNode) && !(node instanceof StatusNode)) return;
private closeRepository(node: RepositoryNode) {
if (!(node instanceof RepositoryNode)) return;
node.repo.closed = true;
}
@ -120,19 +119,19 @@ export class ExplorerCommands implements Disposable {
private compareWithHead(node: ExplorerNode) {
if (!(node instanceof ExplorerRefNode)) return;
Container.resultsExplorer.showComparisonInResults(node.repoPath, node.ref, 'HEAD');
return Container.resultsExplorer.addComparison(node.repoPath, node.ref, 'HEAD');
}
private compareWithRemote(node: BranchNode) {
if (!node.branch.tracking) return;
Container.resultsExplorer.showComparisonInResults(node.repoPath, node.branch.tracking, node.ref);
return Container.resultsExplorer.addComparison(node.repoPath, node.branch.tracking, node.ref);
}
private compareWithWorking(node: ExplorerNode) {
if (!(node instanceof ExplorerRefNode)) return;
Container.resultsExplorer.showComparisonInResults(node.repoPath, node.ref, '');
return Container.resultsExplorer.addComparison(node.repoPath, node.ref, '');
}
private async compareAncestryWithWorking(node: BranchNode) {
@ -142,7 +141,7 @@ export class ExplorerCommands implements Disposable {
const commonAncestor = await Container.git.getMergeBase(node.repoPath, branch.ref, node.ref);
if (commonAncestor === undefined) return;
Container.resultsExplorer.showComparisonInResults(
return Container.resultsExplorer.addComparison(
node.repoPath,
{ ref: commonAncestor, label: `ancestry with ${node.ref} (${GitService.shortenSha(commonAncestor)})` },
''
@ -172,7 +171,7 @@ export class ExplorerCommands implements Disposable {
return;
}
Container.resultsExplorer.showComparisonInResults(this._selection.repoPath, this._selection.ref, node.ref);
return Container.resultsExplorer.addComparison(this._selection.repoPath, this._selection.ref, node.ref);
}
private _selection: ICompareSelected | undefined;

+ 80
- 0
src/views/fileHistoryExplorer.ts Целия файл

@ -0,0 +1,80 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, IExplorersConfig, IFileHistoryExplorerConfig } from '../configuration';
import { CommandContext, setCommandContext } from '../constants';
import { Container } from '../container';
import { ExplorerBase, RefreshReason } from './explorer';
import { RefreshNodeCommandArgs } from './explorerCommands';
import { ActiveFileHistoryNode, ExplorerNode } from './nodes';
export class FileHistoryExplorer extends ExplorerBase<ActiveFileHistoryNode> {
constructor() {
super('gitlens.fileHistoryExplorer');
}
getRoot() {
return new ActiveFileHistoryNode(this);
}
protected registerCommands() {
Container.explorerCommands;
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this);
commands.registerCommand(
this.getQualifiedCommand('refreshNode'),
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args),
this
);
commands.registerCommand(
this.getQualifiedCommand('setRenameFollowingOn'),
() => this.setRenameFollowing(true),
this
);
commands.registerCommand(
this.getQualifiedCommand('setRenameFollowingOff'),
() => this.setRenameFollowing(false),
this
);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (
!initializing &&
!configuration.changed(e, configuration.name('fileHistoryExplorer').value) &&
!configuration.changed(e, configuration.name('explorers').value) &&
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) &&
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value)
) {
return;
}
if (
initializing ||
configuration.changed(e, configuration.name('fileHistoryExplorer')('enabled').value) ||
configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value)
) {
setCommandContext(CommandContext.FileHistoryExplorer, this.config.enabled ? this.config.location : false);
}
if (initializing || configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value)) {
this.initialize(this.config.location);
}
if (!initializing && this._root !== undefined) {
void this.refresh(RefreshReason.ConfigurationChanged);
}
}
get config(): IExplorersConfig & IFileHistoryExplorerConfig {
return { ...Container.config.explorers, ...Container.config.fileHistoryExplorer };
}
private setRenameFollowing(enabled: boolean) {
return configuration.updateEffective(
configuration.name('advanced')('fileHistoryFollowsRenames').value,
enabled
);
}
}

+ 29
- 200
src/views/gitExplorer.ts Целия файл

@ -1,88 +1,64 @@
'use strict';
import {
commands,
ConfigurationChangeEvent,
Disposable,
Event,
EventEmitter,
TextDocumentShowOptions,
TreeDataProvider,
TreeItem,
TreeView,
Uri,
window
} from 'vscode';
import { commands, ConfigurationChangeEvent, Event, EventEmitter } from 'vscode';
import { configuration, ExplorerFilesLayout, IExplorersConfig, IGitExplorerConfig } from '../configuration';
import { CommandContext, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { GitUri } from '../git/gitService';
import { Logger } from '../logger';
import { Functions } from '../system';
import { RefreshNodeCommandArgs } from '../views/explorerCommands';
import { ExplorerNode, MessageNode, RefreshReason, RepositoriesNode, RepositoryNode } from './nodes';
import { ExplorerBase, RefreshReason } from './explorer';
import { RefreshNodeCommandArgs } from './explorerCommands';
import { RepositoriesNode } from './nodes';
import { ExplorerNode } from './nodes/explorerNode';
export * from './nodes';
export interface OpenFileRevisionCommandArgs {
uri?: Uri;
showOptions?: TextDocumentShowOptions;
}
export class GitExplorer implements TreeDataProvider<ExplorerNode>, Disposable {
private _disposable: Disposable | undefined;
private _root?: ExplorerNode;
private _tree: TreeView<ExplorerNode> | undefined;
export class GitExplorer extends ExplorerBase<RepositoriesNode> {
constructor() {
super('gitlens.gitExplorer');
}
private _onDidChangeAutoRefresh = new EventEmitter<void>();
public get onDidChangeAutoRefresh(): Event<void> {
return this._onDidChangeAutoRefresh.event;
}
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> {
return this._onDidChangeTreeData.event;
getRoot() {
return new RepositoriesNode(this);
}
constructor() {
protected registerCommands() {
Container.explorerCommands;
commands.registerCommand('gitlens.gitExplorer.refresh', this.refresh, this);
commands.registerCommand('gitlens.gitExplorer.refreshNode', this.refreshNode, this);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this);
commands.registerCommand(
'gitlens.gitExplorer.setFilesLayoutToAuto',
this.getQualifiedCommand('refreshNode'),
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args),
this
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToAuto'),
() => this.setFilesLayout(ExplorerFilesLayout.Auto),
this
);
commands.registerCommand(
'gitlens.gitExplorer.setFilesLayoutToList',
this.getQualifiedCommand('setFilesLayoutToList'),
() => this.setFilesLayout(ExplorerFilesLayout.List),
this
);
commands.registerCommand(
'gitlens.gitExplorer.setFilesLayoutToTree',
this.getQualifiedCommand('setFilesLayoutToTree'),
() => this.setFilesLayout(ExplorerFilesLayout.Tree),
this
);
commands.registerCommand(
'gitlens.gitExplorer.setAutoRefreshToOn',
this.getQualifiedCommand('setAutoRefreshToOn'),
() => this.setAutoRefresh(Container.config.gitExplorer.autoRefresh, true),
this
);
commands.registerCommand(
'gitlens.gitExplorer.setAutoRefreshToOff',
this.getQualifiedCommand('setAutoRefreshToOff'),
() => this.setAutoRefresh(Container.config.gitExplorer.autoRefresh, false),
this
);
Container.context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this));
void this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._disposable && this._disposable.dispose();
}
private async onConfigurationChanged(e: ConfigurationChangeEvent) {
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (
@ -102,39 +78,19 @@ export class GitExplorer implements TreeDataProvider, Disposable {
setCommandContext(CommandContext.GitExplorer, this.config.enabled ? this.config.location : false);
}
if (initializing || configuration.changed(e, configuration.name('gitExplorer')('autoRefresh').value)) {
if (configuration.changed(e, configuration.name('gitExplorer')('autoRefresh').value)) {
void this.setAutoRefresh(Container.config.gitExplorer.autoRefresh);
}
if (initializing) {
this.setRoot(await this.getRootNode());
}
if (initializing || configuration.changed(e, configuration.name('gitExplorer')('location').value)) {
if (this._disposable) {
this._disposable.dispose();
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>();
}
this._tree = window.createTreeView(`gitlens.gitExplorer:${this.config.location}`, {
treeDataProvider: this
});
this._disposable = this._tree;
this.initialize(this.config.location);
}
if (!initializing && this._root !== undefined) {
this.refresh(RefreshReason.ConfigurationChanged);
void this.refresh(RefreshReason.ConfigurationChanged);
}
}
private onRepositoriesChanged() {
this.clearRoot();
Logger.log(`GitExplorer.onRepositoriesChanged`);
void this.refresh(RefreshReason.RepoChanged);
}
get autoRefresh() {
return (
this.config.autoRefresh &&
@ -146,75 +102,7 @@ export class GitExplorer implements TreeDataProvider, Disposable {
return { ...Container.config.explorers, ...Container.config.gitExplorer };
}
getParent(): ExplorerNode | undefined {
return undefined;
}
private _loading: Promise<void> | undefined;
async getChildren(node?: ExplorerNode): Promise<ExplorerNode[]> {
if (this._loading !== undefined) {
await this._loading;
this._loading = undefined;
}
if (this._root === undefined) {
return [new MessageNode('No repositories found')];
}
if (node === undefined) return this._root.getChildren();
return node.getChildren();
}
async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
return node.getTreeItem();
}
getQualifiedCommand(command: string) {
return `gitlens.gitExplorer.${command}`;
}
async refresh(reason?: RefreshReason, root?: ExplorerNode) {
if (reason === undefined) {
reason = RefreshReason.Command;
}
Logger.log(`GitExplorer.refresh`, `reason='${reason}'`);
if (this._root === undefined) {
this.clearRoot();
this.setRoot(await this.getRootNode());
}
if (this._root !== undefined) {
this._root.refresh();
}
this._onDidChangeTreeData.fire();
}
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
Logger.log(`GitExplorer.refreshNode(${(node as { id?: string }).id || ''})`);
if (args !== undefined && node.supportsPaging) {
node.maxCount = args.maxCount;
}
node.refresh();
// Since the root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(node === this._root ? undefined : node);
}
private _autoRefreshDisposable: Disposable | undefined;
async setAutoRefresh(enabled: boolean, workspaceEnabled?: boolean) {
if (this._autoRefreshDisposable !== undefined) {
this._autoRefreshDisposable.dispose();
this._autoRefreshDisposable = undefined;
}
let toggled = false;
private async setAutoRefresh(enabled: boolean, workspaceEnabled?: boolean) {
if (enabled) {
if (workspaceEnabled === undefined) {
workspaceEnabled = Container.context.workspaceState.get<boolean>(
@ -223,75 +111,16 @@ export class GitExplorer implements TreeDataProvider, Disposable {
);
}
else {
toggled = workspaceEnabled;
await Container.context.workspaceState.update(WorkspaceState.GitExplorerAutoRefresh, workspaceEnabled);
this._onDidChangeAutoRefresh.fire();
}
if (workspaceEnabled) {
this._autoRefreshDisposable = Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this);
Container.context.subscriptions.push(this._autoRefreshDisposable);
}
}
setCommandContext(CommandContext.GitExplorerAutoRefresh, enabled && workspaceEnabled);
if (toggled) {
void this.refresh(RefreshReason.AutoRefreshChanged);
}
}
async show() {
if (this._root === undefined || this._tree === undefined) return;
const [child] = await this._root!.getChildren();
try {
await this._tree.reveal(child, { select: false });
}
catch (ex) {
Logger.error(ex);
}
}
private clearRoot() {
if (this._root === undefined) return;
this._root.dispose();
this._root = undefined;
}
private async getRootNode(): Promise<ExplorerNode | undefined> {
const promise = Container.git.getRepositories();
this._loading = promise.then(_ => Functions.wait(0));
const repositories = [...(await promise)];
if (repositories.length === 0) return undefined;
const openedRepos = repositories.filter(r => !r.closed);
if (openedRepos.length === 0) return undefined;
if (openedRepos.length === 1) {
const repo = openedRepos[0];
return new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this, true);
}
return new RepositoriesNode(openedRepos, this);
this._onDidChangeAutoRefresh.fire();
}
private setFilesLayout(layout: ExplorerFilesLayout) {
return configuration.updateEffective(configuration.name('gitExplorer')('files')('layout').value, layout);
}
private setRoot(root: ExplorerNode | undefined): boolean {
if (this._root === root) return false;
if (this._root !== undefined) {
this._root.dispose();
}
this._root = root;
return true;
}
}

+ 0
- 251
src/views/historyExplorer.ts Целия файл

@ -1,251 +0,0 @@
'use strict';
import * as path from 'path';
import {
commands,
ConfigurationChangeEvent,
Disposable,
Event,
EventEmitter,
TextEditor,
TreeDataProvider,
TreeItem,
TreeView,
Uri,
window
} from 'vscode';
import { UriComparer } from '../comparers';
import { configuration, IExplorersConfig, IHistoryExplorerConfig } from '../configuration';
import { CommandContext, GlyphChars, setCommandContext } from '../constants';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { Logger } from '../logger';
import { Functions } from '../system';
import { RefreshNodeCommandArgs } from '../views/explorerCommands';
import { ExplorerNode, HistoryNode, MessageNode, RefreshReason } from './nodes';
export * from './nodes';
export class HistoryExplorer implements TreeDataProvider<ExplorerNode>, Disposable {
private _disposable: Disposable | undefined;
private _root?: ExplorerNode;
private _tree: TreeView<ExplorerNode> | undefined;
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> {
return this._onDidChangeTreeData.event;
}
constructor() {
Container.explorerCommands;
commands.registerCommand('gitlens.historyExplorer.refresh', this.refresh, this);
commands.registerCommand('gitlens.historyExplorer.refreshNode', this.refreshNode, this);
commands.registerCommand(
'gitlens.historyExplorer.setRenameFollowingOn',
() => this.setRenameFollowing(true),
this
);
commands.registerCommand(
'gitlens.historyExplorer.setRenameFollowingOff',
() => this.setRenameFollowing(false),
this
);
Container.context.subscriptions.push(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this),
configuration.onDidChange(this.onConfigurationChanged, this)
);
void this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._disposable && this._disposable.dispose();
}
private async onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (
!initializing &&
!configuration.changed(e, configuration.name('historyExplorer').value) &&
!configuration.changed(e, configuration.name('explorers').value) &&
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) &&
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value)
) {
return;
}
if (
initializing ||
configuration.changed(e, configuration.name('historyExplorer')('enabled').value) ||
configuration.changed(e, configuration.name('historyExplorer')('location').value)
) {
setCommandContext(CommandContext.HistoryExplorer, this.config.enabled ? this.config.location : false);
}
if (initializing) {
this.setRoot(await this.getRootNode(window.activeTextEditor));
}
if (initializing || configuration.changed(e, configuration.name('historyExplorer')('location').value)) {
if (this._disposable) {
this._disposable.dispose();
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>();
}
this._tree = window.createTreeView(`gitlens.historyExplorer:${this.config.location}`, {
treeDataProvider: this
});
this._disposable = this._tree;
}
if (!initializing && this._root !== undefined) {
void this.refresh(RefreshReason.ConfigurationChanged);
}
}
private async onActiveEditorChanged(editor: TextEditor | undefined) {
const root = await this.getRootNode(editor);
if (!this.setRoot(root)) return;
void this.refresh(RefreshReason.ActiveEditorChanged, root);
}
private onVisibleEditorsChanged(editors: TextEditor[]) {
if (this._root === undefined) return;
// If we have no visible editors, or no trackable visible editors reset the view
if (editors.length === 0 || !editors.some(e => e.document && Container.git.isTrackable(e.document.uri))) {
this.clearRoot();
void this.refresh(RefreshReason.VisibleEditorsChanged);
}
}
get config(): IExplorersConfig & IHistoryExplorerConfig {
return { ...Container.config.explorers, ...Container.config.historyExplorer };
}
getParent(element: ExplorerNode): ExplorerNode | undefined {
return undefined;
}
async getChildren(node?: ExplorerNode): Promise<ExplorerNode[]> {
if (this._root === undefined) return [new MessageNode(`No active file ${GlyphChars.Dash} no history to show`)];
if (node === undefined) return this._root.getChildren();
return node.getChildren();
}
async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
return node.getTreeItem();
}
getQualifiedCommand(command: string) {
return `gitlens.historyExplorer.${command}`;
}
async refresh(reason?: RefreshReason, root?: ExplorerNode) {
if (reason === undefined) {
reason = RefreshReason.Command;
}
Logger.log(`HistoryExplorer.refresh`, `reason='${reason}'`);
if (this._root === undefined || root === undefined) {
this.clearRoot();
this.setRoot(await this.getRootNode(window.activeTextEditor));
}
this._onDidChangeTreeData.fire();
}
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
Logger.log(`HistoryExplorer.refreshNode(${(node as { id?: string }).id || ''})`);
if (args !== undefined && node.supportsPaging) {
node.maxCount = args.maxCount;
}
node.refresh();
// Since a root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(this._root === node ? undefined : node);
}
async show() {
if (this._root === undefined || this._tree === undefined) return;
try {
await this._tree.reveal(this._root, { select: false });
}
catch (ex) {
Logger.error(ex);
}
}
private clearRoot() {
if (this._root === undefined) return;
this._root.dispose();
this._root = undefined;
}
private async getRootNode(editor: TextEditor | undefined): Promise<ExplorerNode | undefined> {
// If we have no active editor, or no visible editors, or no trackable visible editors reset the view
if (
editor == null ||
window.visibleTextEditors.length === 0 ||
!window.visibleTextEditors.some(e => e.document && Container.git.isTrackable(e.document.uri))
) {
return undefined;
}
// If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes)
if (editor.document === undefined || !Container.git.isTrackable(editor.document.uri)) return this._root;
let gitUri = await GitUri.fromUri(editor.document.uri);
const repo = await Container.git.getRepository(gitUri);
if (repo === undefined) return undefined;
let uri;
if (gitUri.sha !== undefined) {
// If we have a sha, normalize the history to the working file (so we get a full history all the time)
const [fileName, repoPath] = await Container.git.findWorkingFileName(
gitUri.fsPath,
gitUri.repoPath,
gitUri.sha
);
if (fileName !== undefined) {
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName);
}
}
if (UriComparer.equals(uri || gitUri, this._root && this._root.uri)) return this._root;
if (uri !== undefined) {
gitUri = await GitUri.fromUri(uri);
}
return new HistoryNode(gitUri, repo, this);
}
private setRenameFollowing(enabled: boolean) {
return configuration.updateEffective(
configuration.name('advanced')('fileHistoryFollowsRenames').value,
enabled
);
}
private setRoot(root: ExplorerNode | undefined): boolean {
if (this._root === root) return false;
if (this._root !== undefined) {
this._root.dispose();
}
this._root = root;
return true;
}
}

+ 80
- 0
src/views/lineHistoryExplorer.ts Целия файл

@ -0,0 +1,80 @@
'use strict';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, IExplorersConfig, ILineHistoryExplorerConfig } from '../configuration';
import { CommandContext, setCommandContext } from '../constants';
import { Container } from '../container';
import { ExplorerBase, RefreshReason } from './explorer';
import { RefreshNodeCommandArgs } from './explorerCommands';
import { ExplorerNode } from './nodes';
import { ActiveLineHistoryNode } from './nodes/activeLineHistoryNode';
export class LineHistoryExplorer extends ExplorerBase<ActiveLineHistoryNode> {
constructor() {
super('gitlens.lineHistoryExplorer');
}
getRoot() {
return new ActiveLineHistoryNode(this);
}
protected registerCommands() {
Container.explorerCommands;
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this);
commands.registerCommand(
this.getQualifiedCommand('refreshNode'),
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args),
this
);
commands.registerCommand(
this.getQualifiedCommand('setRenameFollowingOn'),
() => this.setRenameFollowing(true),
this
);
commands.registerCommand(
this.getQualifiedCommand('setRenameFollowingOff'),
() => this.setRenameFollowing(false),
this
);
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (
!initializing &&
!configuration.changed(e, configuration.name('lineHistoryExplorer').value) &&
!configuration.changed(e, configuration.name('explorers').value) &&
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) &&
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value)
) {
return;
}
if (
initializing ||
configuration.changed(e, configuration.name('lineHistoryExplorer')('enabled').value) ||
configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value)
) {
setCommandContext(CommandContext.LineHistoryExplorer, this.config.enabled ? this.config.location : false);
}
if (initializing || configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value)) {
this.initialize(this.config.location);
}
if (!initializing && this._root !== undefined) {
void this.refresh(RefreshReason.ConfigurationChanged);
}
}
get config(): IExplorersConfig & ILineHistoryExplorerConfig {
return { ...Container.config.explorers, ...Container.config.lineHistoryExplorer };
}
private setRenameFollowing(enabled: boolean) {
return configuration.updateEffective(
configuration.name('advanced')('fileHistoryFollowsRenames').value,
enabled
);
}
}

+ 7
- 7
src/views/nodes.ts Целия файл

@ -1,21 +1,22 @@
'use strict';
export * from './nodes/explorerNode';
export * from './nodes/activeRepositoryNode';
export * from './nodes/activeFileHistoryNode';
export * from './nodes/activeLineHistoryNode';
export * from './nodes/branchesNode';
export * from './nodes/branchNode';
export * from './nodes/commitFileNode';
export * from './nodes/commitNode';
export * from './nodes/commitResultsNode';
export * from './nodes/commitsNode';
export * from './nodes/commitsResultsNode';
export * from './nodes/comparisonResultsNode';
export * from './nodes/fileHistoryNode';
export * from './nodes/historyNode';
export * from './nodes/activeFileHistoryNode';
export * from './nodes/remoteNode';
export * from './nodes/remotesNode';
export * from './nodes/repositoriesNode';
export * from './nodes/repositoryNode';
export * from './nodes/resultsCommitNode';
export * from './nodes/resultsCommitsNode';
export * from './nodes/resultsComparisonNode';
export * from './nodes/resultsNode';
export * from './nodes/stashesNode';
export * from './nodes/stashFileNode';
export * from './nodes/stashNode';
@ -23,7 +24,6 @@ export * from './nodes/statusFileCommitsNode';
export * from './nodes/statusFileNode';
export * from './nodes/statusFilesNode';
export * from './nodes/statusFilesResultsNode';
export * from './nodes/statusNode';
export * from './nodes/statusUpstreamNode';
export * from './nodes/tagsNode';
export * from './nodes/tagNode';

+ 108
- 0
src/views/nodes/activeFileHistoryNode.ts Целия файл

@ -0,0 +1,108 @@
'use strict';
import * as path from 'path';
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode';
import { UriComparer } from '../../comparers';
import { Container } from '../../container';
import { GitUri } from '../../git/gitService';
import { Functions } from '../../system';
import { FileHistoryExplorer } from '../fileHistoryExplorer';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode';
import { FileHistoryNode } from './fileHistoryNode';
export class ActiveFileHistoryNode extends SubscribeableExplorerNode<FileHistoryExplorer> {
private _child: FileHistoryNode | undefined;
constructor(explorer: FileHistoryExplorer) {
super(unknownGitUri, explorer);
}
dispose() {
super.dispose();
this.resetChild();
}
resetChild() {
if (this._child !== undefined) {
this._child.dispose();
this._child = undefined;
}
}
async getChildren(): Promise<ExplorerNode[]> {
if (this._child === undefined) {
if (this.uri === unknownGitUri) {
return [new MessageNode('There are no editors open that can provide file history')];
}
this._child = new FileHistoryNode(this.uri, this.explorer);
}
return [this._child];
}
getTreeItem(): TreeItem {
const item = new TreeItem('File History', TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.ActiveFileHistory;
void this.ensureSubscription();
return item;
}
async refresh() {
const editor = window.activeTextEditor;
if (editor == null || !Container.git.isTrackable(editor.document.uri)) {
if (
this.uri === unknownGitUri ||
(Container.git.isTrackable(this.uri) &&
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri)))
) {
return;
}
this._uri = unknownGitUri;
this.resetChild();
return;
}
if (UriComparer.equals(editor!.document.uri, this.uri)) return;
let gitUri = await GitUri.fromUri(editor!.document.uri);
let uri;
if (gitUri.sha !== undefined) {
// If we have a sha, normalize the history to the working file (so we get a full history all the time)
const [fileName, repoPath] = await Container.git.findWorkingFileName(
gitUri.fsPath,
gitUri.repoPath,
gitUri.sha
);
if (fileName !== undefined) {
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName);
}
}
if (this.uri !== unknownGitUri && UriComparer.equals(uri || gitUri, this.uri)) return;
if (uri !== undefined) {
gitUri = await GitUri.fromUri(uri);
}
this._uri = gitUri;
this.resetChild();
}
protected async subscribe() {
return Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this)
);
}
private onActiveEditorChanged(editor: TextEditor | undefined) {
void this.explorer.refreshNode(this);
}
}

+ 116
- 0
src/views/nodes/activeLineHistoryNode.ts Целия файл

@ -0,0 +1,116 @@
'use strict';
import {
Disposable,
Selection,
TextEditor,
TextEditorSelectionChangeEvent,
TreeItem,
TreeItemCollapsibleState,
window
} from 'vscode';
import { UriComparer } from '../../comparers';
import { Container } from '../../container';
import { GitUri } from '../../git/gitService';
import { Functions } from '../../system';
import { LineHistoryExplorer } from '../lineHistoryExplorer';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode';
import { LineHistoryNode } from './lineHistoryNode';
export class ActiveLineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> {
private _child: LineHistoryNode | undefined;
private _selection: Selection | undefined;
constructor(explorer: LineHistoryExplorer) {
super(unknownGitUri, explorer);
}
dispose() {
super.dispose();
this.resetChild();
}
resetChild() {
if (this._child !== undefined) {
this._child.dispose();
this._child = undefined;
}
}
async getChildren(): Promise<ExplorerNode[]> {
if (this._child === undefined) {
if (this.uri === unknownGitUri) {
return [new MessageNode('There are no editors open that can provide line history')];
}
this._child = new LineHistoryNode(this.uri, this._selection!, this.explorer);
}
return [this._child];
}
getTreeItem(): TreeItem {
const item = new TreeItem('Line History', TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.ActiveLineHistory;
void this.ensureSubscription();
return item;
}
async refresh() {
const editor = window.activeTextEditor;
if (editor == null || !Container.git.isTrackable(editor.document.uri)) {
if (
this.uri === unknownGitUri ||
(Container.git.isTrackable(this.uri) &&
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri)))
) {
return;
}
this._uri = unknownGitUri;
this._selection = undefined;
this.resetChild();
return;
}
if (
UriComparer.equals(editor!.document.uri, this.uri) &&
(this._selection !== undefined && editor.selection.isEqual(this._selection))
) {
return;
}
const gitUri = await GitUri.fromUri(editor!.document.uri);
if (
this.uri !== unknownGitUri &&
UriComparer.equals(gitUri, this.uri) &&
(this._selection !== undefined && editor.selection.isEqual(this._selection))
) {
return;
}
this._uri = gitUri;
this._selection = editor.selection;
this.resetChild();
}
protected async subscribe() {
return Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
window.onDidChangeTextEditorSelection(Functions.debounce(this.onSelectionChanged, 500), this)
);
}
private onActiveEditorChanged(editor: TextEditor | undefined) {
void this.explorer.refreshNode(this);
}
private onSelectionChanged(e: TextEditorSelectionChangeEvent) {
void this.explorer.refreshNode(this);
}
}

+ 0
- 97
src/views/nodes/activeRepositoryNode.ts Целия файл

@ -1,97 +0,0 @@
'use strict';
import { TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { isTextEditor } from '../../constants';
import { Container } from '../../container';
import { GitUri } from '../../git/gitService';
import { Functions } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { ExplorerNode } from './explorerNode';
import { RepositoryNode } from './repositoryNode';
export class ActiveRepositoryNode extends ExplorerNode {
private _repositoryNode: RepositoryNode | undefined;
constructor(
private readonly explorer: GitExplorer
) {
super(undefined!);
Container.context.subscriptions.push(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this)
);
void this.onActiveEditorChanged(window.activeTextEditor);
}
dispose() {
super.dispose();
if (this._repositoryNode !== undefined) {
this._repositoryNode.dispose();
this._repositoryNode = undefined;
}
}
get id(): string {
return 'gitlens:repository:active';
}
private async onActiveEditorChanged(editor: TextEditor | undefined) {
if (editor !== undefined && !isTextEditor(editor)) return;
let changed = false;
try {
const repoPath = await Container.git.getActiveRepoPath(editor);
if (repoPath === undefined) {
if (this._repositoryNode !== undefined) {
changed = true;
this._repositoryNode.dispose();
this._repositoryNode = undefined;
}
return;
}
if (this._repositoryNode !== undefined && this._repositoryNode.repo.path === repoPath) return;
const repo = await Container.git.getRepository(repoPath);
if (repo === undefined || repo.closed) {
if (this._repositoryNode !== undefined) {
changed = true;
this._repositoryNode.dispose();
this._repositoryNode = undefined;
}
return;
}
changed = true;
if (this._repositoryNode !== undefined) {
this._repositoryNode.dispose();
}
this._repositoryNode = new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer, true, this);
}
finally {
if (changed) {
this.explorer.refreshNode(this);
}
}
}
async getChildren(): Promise<ExplorerNode[]> {
return this._repositoryNode !== undefined ? this._repositoryNode.getChildren() : [];
}
getTreeItem(): TreeItem {
const item =
this._repositoryNode !== undefined
? this._repositoryNode.getTreeItem()
: new TreeItem('No active repository', TreeItemCollapsibleState.None);
item.id = this.id;
return item;
}
}

+ 24
- 12
src/views/nodes/branchNode.ts Целия файл

@ -7,19 +7,28 @@ import { GitBranch, GitUri } from '../../git/gitService';
import { Arrays, Iterables } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { CommitNode } from './commitNode';
import { ExplorerNode, ExplorerRefNode, MessageNode, ResourceType, ShowAllNode } from './explorerNode';
import { MessageNode, ShowMoreNode } from './common';
import { ExplorerNode, ExplorerRefNode, PageableExplorerNode, ResourceType } from './explorerNode';
export class BranchNode extends ExplorerRefNode {
export class BranchNode extends ExplorerRefNode implements PageableExplorerNode {
readonly supportsPaging: boolean = true;
maxCount: number | undefined;
constructor(
public readonly branch: GitBranch,
uri: GitUri,
protected readonly explorer: GitExplorer
protected readonly explorer: GitExplorer,
private readonly markCurrent: boolean = true
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.branch.repoPath}):branch(${this.branch.name})${
this.branch.remote ? ':remote' : ''
}${this.markCurrent ? ':current' : ''}`;
}
get current(): boolean {
return this.branch.current;
}
@ -31,16 +40,15 @@ export class BranchNode extends ExplorerRefNode {
return this.current || GitBranch.isDetached(branchName) ? branchName : this.branch.getBasename();
}
get markCurrent(): boolean {
return true;
}
get ref(): string {
return this.branch.ref;
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount, ref: this.ref });
const log = await Container.git.getLog(this.uri.repoPath!, {
maxCount: this.maxCount || this.explorer.config.defaultItemLimit,
ref: this.ref
});
if (log === undefined) return [new MessageNode('No commits yet')];
const branches = await Container.git.getBranches(this.uri.repoPath);
@ -58,12 +66,12 @@ export class BranchNode extends ExplorerRefNode {
return branches.join(', ');
};
const children: (CommitNode | ShowAllNode)[] = [
const children: (CommitNode | ShowMoreNode)[] = [
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer, this.branch, getBranchTips))
];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.explorer));
children.push(new ShowMoreNode('Commits', this, this.explorer));
}
return children;
}
@ -79,8 +87,11 @@ export class BranchNode extends ExplorerRefNode {
GlyphChars.ArrowLeftRightLong
}${GlyphChars.Space} ${this.branch.tracking}`;
}
tooltip += `\n\nTracking ${GlyphChars.Dash} ${this.branch.tracking}
${this.branch.getTrackingStatus({ empty: 'up-to-date', expand: true, separator: '\n' })}`;
tooltip += ` is tracking ${this.branch.tracking}\n${this.branch.getTrackingStatus({
empty: 'up-to-date',
expand: true,
separator: '\n'
})}`;
if (this.branch.state.ahead || this.branch.state.behind) {
if (this.branch.state.behind) {
@ -96,6 +107,7 @@ ${this.branch.getTrackingStatus({ empty: 'up-to-date', expand: true, separator:
`${this.markCurrent && this.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`,
TreeItemCollapsibleState.Collapsed
);
item.id = this.id;
item.tooltip = tooltip;
if (this.branch.remote) {

+ 28
- 5
src/views/nodes/branchOrTagFolderNode.ts Целия файл

@ -2,22 +2,28 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitUri } from '../../git/gitService';
import { Arrays, Objects } from '../../system';
import { Explorer } from '../explorer';
import { BranchNode } from './branchNode';
// import { Container } from '../../container';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { TagNode } from './tagNode';
export class BranchOrTagFolderNode extends ExplorerNode {
constructor(
public readonly type: 'branch' | 'remote-branch' | 'tag',
public readonly repoPath: string,
public readonly folderName: string,
public readonly relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<BranchNode | TagNode>,
private readonly explorer: Explorer
private readonly explorer: Explorer,
private readonly expanded: boolean = false
) {
super(GitUri.fromRepoPath(repoPath));
}
get id(): string {
return `gitlens:repository(${this.repoPath}):${this.type}-folder(${this.folderName})`;
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.root.descendants === undefined || this.root.children === undefined) return [];
@ -25,11 +31,24 @@ export class BranchOrTagFolderNode extends ExplorerNode {
for (const folder of Objects.values(this.root.children)) {
if (folder.value === undefined) {
// If the folder contains the current branch, expand it by default
const expanded =
folder.descendants !== undefined &&
folder.descendants.some(n => n instanceof BranchNode && n.current);
children.push(
new BranchOrTagFolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer)
new BranchOrTagFolderNode(
this.type,
this.repoPath,
folder.name,
folder.relativePath,
folder,
this.explorer,
expanded
)
);
continue;
}
children.push(folder.value);
}
@ -37,7 +56,11 @@ export class BranchOrTagFolderNode extends ExplorerNode {
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
const item = new TreeItem(
this.label,
this.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed
);
item.id = this.id;
item.contextValue = ResourceType.Folder;
item.iconPath = ThemeIcon.Folder;
item.tooltip = this.label;

+ 8
- 18
src/views/nodes/branchesNode.ts Целия файл

@ -13,21 +13,20 @@ export class BranchesNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
private readonly explorer: GitExplorer
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:branches`;
return `gitlens:repository(${this.repo.path}):branches`;
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
branches.sort((a, b) => a.name.localeCompare(b.name));
// filter local branches
const branchNodes = [
@ -35,9 +34,6 @@ export class BranchesNode extends ExplorerNode {
];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return branchNodes;
// Take out the current branch, since that should always be first and un-nested
const current = branchNodes.length > 0 && branchNodes[0].current ? branchNodes.splice(0, 1)[0] : undefined;
const hierarchy = Arrays.makeHierarchical(
branchNodes,
n => (n.branch.detached ? [n.branch.name] : n.branch.getName().split('/')),
@ -45,21 +41,15 @@ export class BranchesNode extends ExplorerNode {
this.explorer.config.files.compact
);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = (await root.getChildren()) as (BranchOrTagFolderNode | BranchNode)[];
// If we found a current branch, insert it at the start
if (current !== undefined) {
children.splice(0, 0, current);
}
return children;
const root = new BranchOrTagFolderNode('branch', this.repo.path, '', undefined, hierarchy, this.explorer);
return root.getChildren();
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Collapsed);
const remotes = await this.repo.getRemotes();
const item = new TreeItem(`Branches`, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue =
remotes !== undefined && remotes.length > 0 ? ResourceType.BranchesWithRemotes : ResourceType.Branches;

+ 11
- 9
src/views/nodes/commitFileNode.ts Целия файл

@ -1,6 +1,6 @@
'use strict';
import * as path from 'path';
import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Command, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
@ -14,7 +14,8 @@ import {
IStatusFormatOptions,
StatusFileFormatter
} from '../../git/gitService';
import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
import { Explorer } from '../explorer';
import { ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
export enum CommitFileNodeDisplayAs {
CommitLabel = 1 << 0,
@ -34,7 +35,8 @@ export class CommitFileNode extends ExplorerRefNode {
public readonly status: IGitStatusFile,
public commit: GitLogCommit,
protected readonly explorer: Explorer,
private displayAs: CommitFileNodeDisplayAs
private readonly _displayAs: CommitFileNodeDisplayAs,
private readonly _selection?: Selection
) {
super(GitUri.fromFileStatus(status, commit.repoPath, commit.sha));
}
@ -69,20 +71,20 @@ export class CommitFileNode extends ExplorerRefNode {
item.contextValue = this.resourceType;
item.tooltip = this.tooltip;
if ((this.displayAs & CommitFileNodeDisplayAs.CommitIcon) === CommitFileNodeDisplayAs.CommitIcon) {
if ((this._displayAs & CommitFileNodeDisplayAs.CommitIcon) === CommitFileNodeDisplayAs.CommitIcon) {
item.iconPath = {
dark: Container.context.asAbsolutePath(path.join('images', 'dark', 'icon-commit.svg')),
light: Container.context.asAbsolutePath(path.join('images', 'light', 'icon-commit.svg'))
};
}
else if ((this.displayAs & CommitFileNodeDisplayAs.StatusIcon) === CommitFileNodeDisplayAs.StatusIcon) {
else if ((this._displayAs & CommitFileNodeDisplayAs.StatusIcon) === CommitFileNodeDisplayAs.StatusIcon) {
const icon = getGitStatusIcon(this.status.status);
item.iconPath = {
dark: Container.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: Container.context.asAbsolutePath(path.join('images', 'light', icon))
};
}
else if ((this.displayAs & CommitFileNodeDisplayAs.Gravatar) === CommitFileNodeDisplayAs.Gravatar) {
else if ((this._displayAs & CommitFileNodeDisplayAs.Gravatar) === CommitFileNodeDisplayAs.Gravatar) {
item.iconPath = this.commit.getGravatarUri(Container.config.defaultGravatarsStyle);
}
@ -107,7 +109,7 @@ export class CommitFileNode extends ExplorerRefNode {
get label() {
if (this._label === undefined) {
this._label =
this.displayAs & CommitFileNodeDisplayAs.CommitLabel
this._displayAs & CommitFileNodeDisplayAs.CommitLabel
? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, {
truncateMessageAtNewLine: true,
dateFormat: Container.config.defaultDateFormat
@ -136,7 +138,7 @@ export class CommitFileNode extends ExplorerRefNode {
private _tooltip: string | undefined;
get tooltip() {
if (this._tooltip === undefined) {
if (this.displayAs & CommitFileNodeDisplayAs.CommitLabel) {
if (this._displayAs & CommitFileNodeDisplayAs.CommitLabel) {
this._tooltip = CommitFormatter.fromTemplate(
this.commit.isUncommitted
? `\${author} ${GlyphChars.Dash} \${id}\n\${ago} (\${date})`
@ -170,7 +172,7 @@ export class CommitFileNode extends ExplorerRefNode {
GitUri.fromFileStatus(this.status, this.commit.repoPath),
{
commit: this.commit,
line: 0,
line: this._selection !== undefined ? this._selection.active.line : 0,
showOptions: {
preserveFocus: true,
preview: true

+ 2
- 1
src/views/nodes/commitNode.ts Целия файл

@ -7,8 +7,9 @@ import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { CommitFormatter, GitBranch, GitLogCommit, ICommitFormatOptions } from '../../git/gitService';
import { Arrays, Iterables, Strings } from '../../system';
import { Explorer } from '../explorer';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
import { ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
import { FolderNode, IFileExplorerNode } from './folderNode';
export class CommitNode extends ExplorerRefNode {

+ 0
- 38
src/views/nodes/commitsNode.ts Целия файл

@ -1,38 +0,0 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitLog, GitUri } from '../../git/gitService';
import { Iterables } from '../../system';
import { CommitNode } from './commitNode';
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
export class CommitsNode extends ExplorerNode {
readonly supportsPaging: boolean = true;
constructor(
public readonly repoPath: string,
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>,
private readonly explorer: Explorer
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await this.logFn(this.maxCount);
if (log === undefined) return [];
const children: (CommitNode | ShowAllNode)[] = [
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))
];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem('Commits', TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Commits;
return item;
}
}

+ 0
- 74
src/views/nodes/commitsResultsNode.ts Целия файл

@ -1,74 +0,0 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitLog, GitUri } from '../../git/gitService';
import { Iterables } from '../../system';
import { CommitNode } from './commitNode';
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
export class CommitsResultsNode extends ExplorerNode {
readonly supportsPaging: boolean = true;
private _cache: { label: string; log: GitLog | undefined } | undefined;
constructor(
public readonly repoPath: string,
private readonly labelFn: (log: GitLog | undefined) => Promise<string>,
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>,
private readonly explorer: Explorer,
private readonly contextValue: ResourceType = ResourceType.ResultsCommits
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await this.getLog();
if (log === undefined) return [];
const children: (CommitNode | ShowAllNode)[] = [
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))
];
if (log.truncated) {
children.push(new ShowAllNode('Show All Results', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const log = await this.getLog();
const item = new TreeItem(
await this.getLabel(),
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None
);
item.contextValue = this.contextValue;
return item;
}
refresh() {
this._cache = undefined;
}
private async ensureCache() {
if (this._cache === undefined) {
const log = await this.logFn(this.maxCount);
this._cache = {
label: await this.labelFn(log),
log: log
};
}
return this._cache;
}
private async getLabel() {
const cache = await this.ensureCache();
return cache.label;
}
private async getLog() {
const cache = await this.ensureCache();
return cache.log;
}
}

+ 94
- 0
src/views/nodes/common.ts Целия файл

@ -0,0 +1,94 @@
import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { Explorer } from '../explorer';
import { RefreshNodeCommandArgs } from '../explorerCommands';
import { ExplorerNode, ResourceType, unknownGitUri } from '../nodes/explorerNode';
export class MessageNode extends ExplorerNode {
constructor(
private readonly message: string,
private readonly tooltip?: string,
private readonly iconPath?:
| string
| Uri
| {
light: string | Uri;
dark: string | Uri;
}
| ThemeIcon
) {
super(unknownGitUri);
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
item.contextValue = ResourceType.Message;
item.tooltip = this.tooltip;
item.iconPath = this.iconPath;
return item;
}
}
export abstract class PagerNode extends ExplorerNode {
protected _args: RefreshNodeCommandArgs = {};
constructor(
protected readonly message: string,
protected readonly node: ExplorerNode,
protected readonly explorer: Explorer
) {
super(unknownGitUri);
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
item.contextValue = ResourceType.Pager;
item.command = this.getCommand();
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-unfold.svg'),
light: Container.context.asAbsolutePath('images/light/icon-unfold.svg')
};
return item;
}
getCommand(): Command | undefined {
return {
title: 'Refresh',
command: this.explorer.getQualifiedCommand('refreshNode'),
arguments: [this.node, this._args]
} as Command;
}
}
export class ShowMoreNode extends PagerNode {
constructor(
type: string,
node: ExplorerNode,
explorer: Explorer,
maxCount: number = Container.config.advanced.maxListItems
) {
super(
maxCount === 0
? `Show All ${type} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`
: `Show More ${type}`,
node,
explorer
);
this._args.maxCount = maxCount;
}
}
export class ShowAllNode extends ShowMoreNode {
constructor(type: string, node: ExplorerNode, explorer: Explorer) {
super(type, node, explorer, 0);
}
}

+ 0
- 59
src/views/nodes/comparisonResultsNode.ts Целия файл

@ -1,59 +0,0 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitLog, GitService, GitUri } from '../../git/gitService';
import { Strings } from '../../system';
import { CommitsResultsNode } from './commitsResultsNode';
import { Explorer, ExplorerNode, NamedRef, ResourceType } from './explorerNode';
import { StatusFilesResultsNode } from './statusFilesResultsNode';
export class ComparisonResultsNode extends ExplorerNode {
constructor(
public readonly repoPath: string,
public readonly ref1: NamedRef,
public readonly ref2: NamedRef,
private readonly explorer: Explorer
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
this.resetChildren();
const commitsQueryFn = (maxCount: number | undefined) =>
Container.git.getLog(this.uri.repoPath!, {
maxCount: maxCount,
ref: `${this.ref1.ref}...${this.ref2.ref || 'HEAD'}`
});
const commitsLabelFn = async (log: GitLog | undefined) => {
const count = log !== undefined ? log.count : 0;
const truncated = log !== undefined ? log.truncated : false;
return Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' });
};
this.children = [
new CommitsResultsNode(this.uri.repoPath!, commitsLabelFn, commitsQueryFn, this.explorer),
new StatusFilesResultsNode(this.uri.repoPath!, this.ref1.ref, this.ref2.ref, this.explorer)
];
return this.children;
}
async getTreeItem(): Promise<TreeItem> {
let repository = '';
if ((await Container.git.getRepositoryCount()) > 1) {
const repo = await Container.git.getRepository(this.uri.repoPath!);
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`;
}
const item = new TreeItem(
`Comparing ${this.ref1.label || GitService.shortenSha(this.ref1.ref, { working: 'Working Tree' })} to ${this
.ref2.label || GitService.shortenSha(this.ref2.ref, { working: 'Working Tree' })}${repository}`,
TreeItemCollapsibleState.Expanded
);
item.contextValue = ResourceType.ComparisonResults;
return item;
}
}

+ 77
- 111
src/views/nodes/explorerNode.ts Целия файл

@ -1,30 +1,11 @@
'use strict';
import { Command, Disposable, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { Command, Disposable, Event, TreeItem, TreeViewVisibilityChangeEvent } from 'vscode';
import { GitUri } from '../../git/gitService';
import { RefreshNodeCommandArgs } from '../explorerCommands';
import { GitExplorer } from '../gitExplorer';
import { HistoryExplorer } from '../historyExplorer';
import { ResultsExplorer } from '../resultsExplorer';
export interface NamedRef {
label?: string;
ref: string;
}
export enum RefreshReason {
ActiveEditorChanged = 'active-editor-changed',
AutoRefreshChanged = 'auto-refresh-changed',
Command = 'command',
ConfigurationChanged = 'configuration',
NodeCommand = 'node-command',
RepoChanged = 'repo-changed',
ViewChanged = 'view-changed',
VisibleEditorsChanged = 'visible-editors-changed'
}
import { Explorer } from '../explorer';
export enum ResourceType {
ActiveFileHistory = 'gitlens:active:history-file',
ActiveLineHistory = 'gitlens:active:history-line',
Branch = 'gitlens:branch',
BranchWithTracking = 'gitlens:branch:tracking',
Branches = 'gitlens:branches',
@ -39,7 +20,6 @@ export enum ResourceType {
ComparisonResults = 'gitlens:results:comparison',
FileHistory = 'gitlens:history-file',
Folder = 'gitlens:folder',
History = 'gitlens:history',
Message = 'gitlens:message',
Pager = 'gitlens:pager',
Remote = 'gitlens:remote',
@ -53,7 +33,6 @@ export enum ResourceType {
Stash = 'gitlens:stash',
StashFile = 'gitlens:file:stash',
Stashes = 'gitlens:stashes',
Status = 'gitlens:status',
StatusFile = 'gitlens:file:status',
StatusFiles = 'gitlens:status:files',
StatusFileCommits = 'gitlens:status:file-commits',
@ -62,31 +41,21 @@ export enum ResourceType {
Tags = 'gitlens:tags'
}
export type Explorer = GitExplorer | HistoryExplorer | ResultsExplorer;
// let id = 0;
export abstract class ExplorerNode implements Disposable {
readonly supportsPaging: boolean = false;
maxCount: number | undefined;
export interface NamedRef {
label?: string;
ref: string;
}
protected children: ExplorerNode[] | undefined;
protected disposable: Disposable | undefined;
// protected readonly id: number;
export const unknownGitUri = new GitUri();
constructor(
public readonly uri: GitUri
) {
// this.id = id++;
export abstract class ExplorerNode {
constructor(uri: GitUri) {
this._uri = uri;
}
dispose() {
if (this.disposable !== undefined) {
this.disposable.dispose();
this.disposable = undefined;
}
this.resetChildren();
protected _uri: GitUri;
get uri() {
return this._uri;
}
abstract getChildren(): ExplorerNode[] | Promise<ExplorerNode[]>;
@ -96,95 +65,92 @@ export abstract class ExplorerNode implements Disposable {
return undefined;
}
refresh(): void {}
resetChildren(): void {
if (this.children !== undefined) {
this.children.forEach(c => c.dispose());
this.children = undefined;
}
}
refresh(): void | Promise<void> {}
}
export abstract class ExplorerRefNode extends ExplorerNode {
abstract get ref(): string;
get repoPath(): string {
return this.uri.repoPath!;
}
}
export class MessageNode extends ExplorerNode {
constructor(
private readonly message: string,
private readonly tooltip?: string,
private readonly iconPath?:
| string
| Uri
| {
light: string | Uri;
dark: string | Uri;
}
| ThemeIcon
) {
super(new GitUri());
}
export interface PageableExplorerNode {
readonly supportsPaging: boolean;
maxCount: number | undefined;
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
}
export function isPageable(
node: ExplorerNode
): node is ExplorerNode & { supportsPaging: boolean; maxCount: number | undefined } {
return !!(node as any).supportsPaging;
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
item.contextValue = ResourceType.Message;
item.tooltip = this.tooltip;
item.iconPath = this.iconPath;
return item;
}
export function supportsAutoRefresh(
explorer: Explorer
): explorer is Explorer & { autoRefresh: boolean; onDidChangeAutoRefresh: Event<void> } {
return (explorer as any).onDidChangeAutoRefresh !== undefined;
}
export class PagerNode extends ExplorerNode {
args: RefreshNodeCommandArgs = {};
export abstract class SubscribeableExplorerNode<TExplorer extends Explorer> extends ExplorerNode {
protected _disposable: Disposable;
protected _subscription: Disposable | undefined;
constructor(
private readonly message: string,
private readonly node: ExplorerNode,
protected readonly explorer: Explorer
uri: GitUri,
protected readonly explorer: TExplorer
) {
super(new GitUri());
super(uri);
const disposables = [this.explorer.onDidChangeVisibility(this.onVisibilityChanged, this)];
if (supportsAutoRefresh(this.explorer)) {
disposables.push(this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this));
}
this._disposable = Disposable.from(...disposables);
}
dispose() {
this.unsubscribe();
if (this._disposable !== undefined) {
this._disposable.dispose();
}
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
protected abstract async subscribe(): Promise<Disposable | undefined>;
protected unsubscribe(): void {
if (this._subscription !== undefined) {
this._subscription.dispose();
this._subscription = undefined;
}
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
item.contextValue = ResourceType.Pager;
item.command = this.getCommand();
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-unfold.svg'),
light: Container.context.asAbsolutePath('images/light/icon-unfold.svg')
};
return item;
protected onAutoRefreshChanged() {
this.onVisibilityChanged({ visible: this.explorer.visible });
}
getCommand(): Command | undefined {
return {
title: 'Refresh',
command: this.explorer.getQualifiedCommand('refreshNode'),
arguments: [this.node, this.args]
} as Command;
protected onVisibilityChanged(e: TreeViewVisibilityChangeEvent) {
void this.ensureSubscription();
if (e.visible) {
void this.explorer.refreshNode(this);
}
}
}
export class ShowAllNode extends PagerNode {
args: RefreshNodeCommandArgs = { maxCount: 0 };
async ensureSubscription() {
// We only need to subscribe if we are visible and if auto-refresh enabled (when supported)
if (!this.explorer.visible || (supportsAutoRefresh(this.explorer) && !this.explorer.autoRefresh)) {
this.unsubscribe();
return;
}
// If we already have a subscription, just kick out
if (this._subscription !== undefined) return;
constructor(message: string, node: ExplorerNode, explorer: Explorer) {
super(
`${message} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`,
node,
explorer
);
this._subscription = await this.subscribe();
}
}

+ 23
- 24
src/views/nodes/fileHistoryNode.ts Целия файл

@ -6,28 +6,23 @@ import {
GitLogCommit,
GitService,
GitUri,
Repository,
RepositoryChange,
RepositoryChangeEvent,
RepositoryFileSystemChangeEvent
} from '../../git/gitService';
import { Logger } from '../../logger';
import { Iterables } from '../../system';
import { FileHistoryExplorer } from '../fileHistoryExplorer';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { Explorer, ExplorerNode, MessageNode, ResourceType } from './explorerNode';
export class FileHistoryNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: Explorer
) {
super(uri);
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode';
export class FileHistoryNode extends SubscribeableExplorerNode<FileHistoryExplorer> {
constructor(uri: GitUri, explorer: FileHistoryExplorer) {
super(uri, explorer);
}
async getChildren(): Promise<ExplorerNode[]> {
this.updateSubscription();
const children: ExplorerNode[] = [];
const displayAs =
@ -52,12 +47,13 @@ export class FileHistoryNode extends ExplorerNode {
previousSha = 'HEAD';
}
const user = await Container.git.getCurrentUser(this.uri.repoPath!);
const commit = new GitLogCommit(
GitCommitType.File,
this.uri.repoPath!,
sha,
'You',
undefined,
user !== undefined ? user.email : undefined,
new Date(),
'',
status.fileName,
@ -85,8 +81,6 @@ export class FileHistoryNode extends ExplorerNode {
}
getTreeItem(): TreeItem {
this.updateSubscription();
const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.FileHistory;
item.tooltip = `History of ${this.uri.getFilename()}\n${this.uri.getDirectory()}/`;
@ -96,19 +90,24 @@ export class FileHistoryNode extends ExplorerNode {
light: Container.context.asAbsolutePath('images/light/icon-history.svg')
};
void this.ensureSubscription();
return item;
}
private updateSubscription() {
if (this.disposable) return;
protected async subscribe() {
const repo = await Container.git.getRepository(this.uri);
if (repo === undefined) return undefined;
this.disposable = Disposable.from(
this.repo.onDidChange(this.onRepoChanged, this),
this.repo.onDidChangeFileSystem(this.onRepoFileSystemChanged, this),
{ dispose: () => this.repo.stopWatchingFileSystem() }
const subscription = Disposable.from(
repo.onDidChange(this.onRepoChanged, this),
repo.onDidChangeFileSystem(this.onRepoFileSystemChanged, this),
{ dispose: () => repo.stopWatchingFileSystem() }
);
this.repo.startWatchingFileSystem();
repo.startWatchingFileSystem();
return subscription;
}
private onRepoChanged(e: RepositoryChangeEvent) {
@ -116,7 +115,7 @@ export class FileHistoryNode extends ExplorerNode {
Logger.log(`FileHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);
this.explorer.refreshNode(this);
void this.explorer.refreshNode(this);
}
private onRepoFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
@ -124,6 +123,6 @@ export class FileHistoryNode extends ExplorerNode {
Logger.log(`FileHistoryNode.onRepoFileSystemChanged; triggering node refresh`);
this.explorer.refreshNode(this);
void this.explorer.refreshNode(this);
}
}

+ 2
- 1
src/views/nodes/folderNode.ts Целия файл

@ -3,7 +3,8 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerFilesLayout, IExplorersFilesConfig } from '../../configuration';
import { GitUri } from '../../git/gitService';
import { Arrays, Objects } from '../../system';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { Explorer } from '../explorer';
import { ExplorerNode, ResourceType } from './explorerNode';
export interface IFileExplorerNode extends ExplorerNode {
folderName: string;

+ 0
- 35
src/views/nodes/historyNode.ts Целия файл

@ -1,35 +0,0 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { GitUri, Repository } from '../../git/gitService';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { FileHistoryNode } from './fileHistoryNode';
export class HistoryNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: Explorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
this.resetChildren();
this.children = [new FileHistoryNode(this.uri, this.repo, this.explorer)];
return this.children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.History;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'),
light: Container.context.asAbsolutePath('images/light/icon-history.svg')
};
return item;
}
}

+ 131
- 0
src/views/nodes/lineHistoryNode.ts Целия файл

@ -0,0 +1,131 @@
'use strict';
import { Disposable, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { GitCommitType, GitLogCommit, IGitStatusFile } from '../../git/git';
import { GitUri, RepositoryChange, RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../git/gitService';
import { Logger } from '../../logger';
import { Iterables } from '../../system';
import { LineHistoryExplorer } from '../lineHistoryExplorer';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode';
export class LineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> {
constructor(
uri: GitUri,
public readonly selection: Selection,
explorer: LineHistoryExplorer
) {
super(uri, explorer);
}
async getChildren(): Promise<ExplorerNode[]> {
const children: ExplorerNode[] = [];
const displayAs =
CommitFileNodeDisplayAs.CommitLabel |
(this.explorer.config.avatars ? CommitFileNodeDisplayAs.Gravatar : CommitFileNodeDisplayAs.StatusIcon);
const log = await Container.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, {
ref: this.uri.sha,
range: this.selection
});
if (log !== undefined) {
children.push(
...Iterables.filterMap(
log.commits.values(),
c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, displayAs, this.selection)
)
);
}
const blame = await Container.git.getBlameForLine(this.uri, this.selection.active.line);
if (blame !== undefined) {
const first = children[0] as CommitFileNode | undefined;
if (first === undefined || first.commit.sha !== blame.commit.sha) {
const status: IGitStatusFile = {
fileName: blame.commit.fileName,
indexStatus: '?',
originalFileName: blame.commit.originalFileName,
repoPath: this.uri.repoPath!,
status: 'M',
workTreeStatus: '?'
};
const commit = new GitLogCommit(
GitCommitType.File,
this.uri.repoPath!,
blame.commit.sha,
'You',
blame.commit.email,
blame.commit.date,
blame.commit.message,
blame.commit.fileName,
[status],
'M',
blame.commit.originalFileName,
blame.commit.previousSha,
blame.commit.originalFileName || blame.commit.fileName
);
children.splice(0, 0, new CommitFileNode(status, commit, this.explorer, displayAs, this.selection));
}
}
if (children.length === 0) return [new MessageNode('No line history')];
return children;
}
getTreeItem(): TreeItem {
const lines = this.selection.isSingleLine
? ` #${this.selection.start.line + 1}`
: ` #${this.selection.start.line + 1}-${this.selection.end.line + 1}`;
const item = new TreeItem(
`${this.uri.getFormattedPath({ suffix: `${lines}${this.uri.sha ? ` (${this.uri.shortSha})` : ''}` })}`,
TreeItemCollapsibleState.Expanded
);
item.contextValue = ResourceType.FileHistory;
item.tooltip = `History of ${this.uri.getFilename()}${lines}\n${this.uri.getDirectory()}/`;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'),
light: Container.context.asAbsolutePath('images/light/icon-history.svg')
};
void this.ensureSubscription();
return item;
}
protected async subscribe() {
const repo = await Container.git.getRepository(this.uri);
if (repo === undefined) return undefined;
const subscription = Disposable.from(
repo.onDidChange(this.onRepoChanged, this),
repo.onDidChangeFileSystem(this.onRepoFileSystemChanged, this),
{ dispose: () => repo.stopWatchingFileSystem() }
);
repo.startWatchingFileSystem();
return subscription;
}
private onRepoChanged(e: RepositoryChangeEvent) {
if (!e.changed(RepositoryChange.Repository)) return;
Logger.log(`LineHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);
void this.explorer.refreshNode(this);
}
private onRepoFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
if (!e.uris.some(uri => uri.toString(true) === this.uri.toString(true))) return;
Logger.log(`LineHistoryNode.onRepoFileSystemChanged; triggering node refresh`);
void this.explorer.refreshNode(this);
}
}

+ 13
- 1
src/views/nodes/remoteNode.ts Целия файл

@ -20,6 +20,10 @@ export class RemoteNode extends ExplorerNode {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.remote.repoPath}):remote(${this.remote.name})`;
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
@ -45,7 +49,14 @@ export class RemoteNode extends ExplorerNode {
this.explorer.config.files.compact
);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const root = new BranchOrTagFolderNode(
'remote-branch',
this.repo.path,
'',
undefined,
hierarchy,
this.explorer
);
const children = (await root.getChildren()) as (BranchOrTagFolderNode | BranchNode)[];
return children;
@ -74,6 +85,7 @@ export class RemoteNode extends ExplorerNode {
} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.path}`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ResourceType.Remote;
item.tooltip = `${this.remote.name}
${this.remote.path} (${this.remote.provider !== undefined ? this.remote.provider.name : this.remote.domain})`;

+ 5
- 4
src/views/nodes/remotesNode.ts Целия файл

@ -4,21 +4,21 @@ import { Container } from '../../container';
import { GitUri, Repository } from '../../git/gitService';
import { Iterables } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType } from './explorerNode';
import { RemoteNode } from './remoteNode';
export class RemotesNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
private readonly explorer: GitExplorer
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:remotes`;
return `gitlens:repository(${this.repo.path}):remotes`;
}
async getChildren(): Promise<ExplorerNode[]> {
@ -31,6 +31,7 @@ export class RemotesNode extends ExplorerNode {
getTreeItem(): TreeItem {
const item = new TreeItem(`Remotes`, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ResourceType.Remotes;
item.iconPath = {

+ 109
- 23
src/views/nodes/repositoriesNode.ts Целия файл

@ -1,41 +1,127 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitUri, Repository } from '../../git/gitService';
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { Container } from '../../container';
import { GitUri } from '../../git/gitService';
import { Logger } from '../../logger';
import { Functions } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { ActiveRepositoryNode } from './activeRepositoryNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode';
import { RepositoryNode } from './repositoryNode';
export class RepositoriesNode extends ExplorerNode {
constructor(
private readonly repositories: Repository[],
private readonly explorer: GitExplorer
) {
super(undefined!);
export class RepositoriesNode extends SubscribeableExplorerNode<GitExplorer> {
private _children: (RepositoryNode | MessageNode)[] | undefined;
constructor(explorer: GitExplorer) {
super(unknownGitUri, explorer);
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.children === undefined) {
this.children = this.repositories
.sort((a, b) => a.index - b.index)
.filter(repo => !repo.closed)
.map(repo => new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer));
if (this.children.length > 1) {
this.children.splice(0, 0, new ActiveRepositoryNode(this.explorer));
dispose() {
super.dispose();
if (this._children !== undefined) {
for (const child of this._children) {
if (child instanceof RepositoryNode) {
child.dispose();
}
}
this._children = undefined;
}
return this.children;
}
refresh() {
this.resetChildren();
async getChildren(): Promise<ExplorerNode[]> {
if (this._children === undefined) {
const repositories = [...(await Container.git.getRepositories())];
if (repositories.length === 0) return [new MessageNode('No repositories found')];
const children = [];
for (const repo of repositories.sort((a, b) => a.index - b.index)) {
if (repo.closed) continue;
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer));
}
this._children = children;
}
return this._children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(`Repositories`, TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.Repositories;
void this.ensureSubscription();
return item;
}
async refresh() {
if (this._children === undefined) return;
const repositories = [...(await Container.git.getRepositories())];
if (repositories.length === 0 && (this._children === undefined || this._children.length === 0)) return;
if (repositories.length === 0) {
this._children = [new MessageNode('No repositories found')];
return;
}
const children = [];
for (const repo of repositories.sort((a, b) => a.index - b.index)) {
const normalizedPath = repo.normalizedPath;
const child = (this._children as RepositoryNode[]).find(c => c.repo.normalizedPath === normalizedPath);
if (child !== undefined) {
children.push(child);
child.refresh();
}
else {
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer));
}
}
for (const child of this._children as RepositoryNode[]) {
if (children.includes(child)) continue;
child.dispose();
}
this._children = children;
void this.ensureSubscription();
}
protected async subscribe() {
return Disposable.from(
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this),
Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this)
);
}
private async onActiveEditorChanged(editor: TextEditor | undefined) {
if (editor == null || this._children === undefined || this._children.length === 1) {
return;
}
try {
const uri = editor.document.uri;
const gitUri = await Container.git.getVersionedUri(uri);
const node = this._children.find(n => n instanceof RepositoryNode && n.repo.containsUri(gitUri || uri)) as
| RepositoryNode
| undefined;
if (node === undefined) return;
// HACK: Since we have no expand/collapse api, reveal the first child to force an expand
// See https://github.com/Microsoft/vscode/issues/55879
const children = await node.getChildren();
await this.explorer.reveal(children !== undefined && children.length !== 0 ? children[0] : node);
}
catch (ex) {
Logger.error(ex);
}
}
private onRepositoriesChanged() {
void this.explorer.refreshNode(this);
}
}

+ 157
- 54
src/views/nodes/repositoryNode.ts Целия файл

@ -1,119 +1,222 @@
'use strict';
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../../constants';
import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../../git/gitService';
import { Container } from '../../container';
import {
GitBranch,
GitStatus,
GitUri,
Repository,
RepositoryChange,
RepositoryChangeEvent,
RepositoryFileSystemChangeEvent
} from '../../git/gitService';
import { Logger } from '../../logger';
import { Strings } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { BranchesNode } from './branchesNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { BranchNode } from './branchNode';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode';
import { RemotesNode } from './remotesNode';
import { StashesNode } from './stashesNode';
import { StatusNode } from './statusNode';
import { StatusFilesNode } from './statusFilesNode';
import { StatusUpstreamNode } from './statusUpstreamNode';
import { TagsNode } from './tagsNode';
export class RepositoryNode extends ExplorerNode {
export class RepositoryNode extends SubscribeableExplorerNode<GitExplorer> {
private _children: ExplorerNode[] | undefined;
private _status: Promise<GitStatus | undefined>;
constructor(
uri: GitUri,
public readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false,
private readonly activeParent?: ExplorerNode
explorer: GitExplorer
) {
super(uri);
super(uri, explorer);
this._status = this.repo.getStatus();
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}`;
return `gitlens:repository(${this.repo.path})`;
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.children === undefined) {
this.updateSubscription();
this.children = [
new StatusNode(this.uri, this.repo, this.explorer, this.active),
new BranchesNode(this.uri, this.repo, this.explorer, this.active),
new RemotesNode(this.uri, this.repo, this.explorer, this.active),
new StashesNode(this.uri, this.repo, this.explorer, this.active),
new TagsNode(this.uri, this.repo, this.explorer, this.active)
];
if (this._children === undefined) {
const children = [];
const status = await this._status;
if (status !== undefined) {
const branch = new GitBranch(
status.repoPath,
status.branch,
true,
status.sha,
status.upstream,
status.state.ahead,
status.state.behind,
status.detached
);
children.push(new BranchNode(branch, this.uri, this.explorer, false));
if (status.state.behind) {
children.push(new StatusUpstreamNode(status, 'behind', this.explorer));
}
if (status.state.ahead) {
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer));
}
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) {
const range = status.upstream ? `${status.upstream}..${branch.ref}` : undefined;
children.push(new StatusFilesNode(status, range, this.explorer));
}
children.push(new MessageNode(GlyphChars.Dash.repeat(2), ''));
}
children.push(
new BranchesNode(this.uri, this.repo, this.explorer),
new RemotesNode(this.uri, this.repo, this.explorer),
new StashesNode(this.uri, this.repo, this.explorer),
new TagsNode(this.uri, this.repo, this.explorer)
);
this._children = children;
}
return this.children;
return this._children;
}
getTreeItem(): TreeItem {
this.updateSubscription();
async getTreeItem(): Promise<TreeItem> {
let label = this.repo.formattedName || this.uri.repoPath || '';
let tooltip = this.repo.formattedName ? `${this.repo.formattedName}\n${this.uri.repoPath}` : this.uri.repoPath;
let iconSuffix = '';
let workingStatus = '';
const status = await this._status;
if (status !== undefined) {
tooltip += `\n\n${status.branch}`;
if (status.files.length !== 0 && this.includeWorkingTree) {
workingStatus = status.getFormattedDiffStatus({
compact: true,
prefix: Strings.pad(GlyphChars.Dot, 2, 2)
});
}
const label = this.active
? `Active Repository ${Strings.pad(GlyphChars.Dash, 1, 1)} ${this.repo.formattedName || this.uri.repoPath}`
: `${this.repo.formattedName || this.uri.repoPath}`;
const upstreamStatus = status.getUpstreamStatus({
prefix: `${GlyphChars.Space} `
});
label += ` ${Strings.pad(GlyphChars.Dash, 2, 3)}${status.branch}${upstreamStatus}${workingStatus}`;
iconSuffix = workingStatus ? '-blue' : '';
if (status.upstream !== undefined) {
tooltip += ` is tracking ${status.upstream}\n${status.getUpstreamStatus({
empty: 'up-to-date',
expand: true,
separator: '\n',
suffix: '\n'
})}`;
if (status.state.behind) {
iconSuffix = '-red';
}
if (status.state.ahead) {
iconSuffix = status.state.behind ? '-yellow' : '-green';
}
}
const item = new TreeItem(
label,
this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed
);
if (workingStatus) {
tooltip += `\nWorking tree has uncommitted changes${status.getFormattedDiffStatus({
expand: true,
prefix: `\n`,
separator: '\n'
})}`;
}
}
const item = new TreeItem(label, TreeItemCollapsibleState.Expanded);
item.id = this.id;
item.contextValue = ResourceType.Repository;
item.tooltip = tooltip;
item.iconPath = {
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`),
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`)
};
void this.ensureSubscription();
return item;
}
refresh() {
this.resetChildren();
this.updateSubscription();
this._status = this.repo.getStatus();
this._children = undefined;
void this.ensureSubscription();
}
private updateSubscription() {
// We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed
if (this.explorer.autoRefresh) {
this.disposable =
this.disposable ||
Disposable.from(
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this),
this.repo.onDidChange(this.onRepoChanged, this)
);
}
else if (this.disposable !== undefined) {
this.disposable.dispose();
this.disposable = undefined;
protected async subscribe() {
const disposables = [this.repo.onDidChange(this.onRepoChanged, this)];
if (this.includeWorkingTree) {
disposables.push(this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), {
dispose: () => this.repo.stopWatchingFileSystem()
});
this.repo.startWatchingFileSystem();
}
return Disposable.from(...disposables);
}
private get includeWorkingTree(): boolean {
return this.explorer.config.includeWorkingTree;
}
private onAutoRefreshChanged() {
this.updateSubscription();
private onFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
void this.explorer.refreshNode(this);
}
private onRepoChanged(e: RepositoryChangeEvent) {
Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`);
if (e.changed(RepositoryChange.Closed)) {
this.dispose();
return;
}
if (
this.children === undefined ||
this._children === undefined ||
e.changed(RepositoryChange.Repository) ||
e.changed(RepositoryChange.Config)
) {
this.explorer.refreshNode(this.active && this.activeParent !== undefined ? this.activeParent : this);
void this.explorer.refreshNode(this);
return;
}
if (e.changed(RepositoryChange.Stashes)) {
const node = this.children.find(c => c instanceof StashesNode);
const node = this._children.find(c => c instanceof StashesNode);
if (node !== undefined) {
this.explorer.refreshNode(node);
void this.explorer.refreshNode(node);
}
}
if (e.changed(RepositoryChange.Remotes)) {
const node = this.children.find(c => c instanceof RemotesNode);
const node = this._children.find(c => c instanceof RemotesNode);
if (node !== undefined) {
this.explorer.refreshNode(node);
void this.explorer.refreshNode(node);
}
}
if (e.changed(RepositoryChange.Tags)) {
const node = this.children.find(c => c instanceof TagsNode);
const node = this._children.find(c => c instanceof TagsNode);
if (node !== undefined) {
this.explorer.refreshNode(node);
void this.explorer.refreshNode(node);
}
}
}

src/views/nodes/commitResultsNode.ts → src/views/nodes/resultsCommitNode.ts Целия файл

@ -5,7 +5,7 @@ import { ResultsExplorer } from '../resultsExplorer';
import { CommitNode } from './commitNode';
import { ExplorerNode, ResourceType } from './explorerNode';
export class CommitResultsNode extends ExplorerNode {
export class ResultsCommitNode extends ExplorerNode {
constructor(
public readonly commit: GitLogCommit,
private readonly explorer: ResultsExplorer

+ 68
- 0
src/views/nodes/resultsCommitsNode.ts Целия файл

@ -0,0 +1,68 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitLog, GitUri } from '../../git/gitService';
import { Iterables } from '../../system';
import { ResultsExplorer } from '../resultsExplorer';
import { CommitNode } from './commitNode';
import { ShowAllNode } from './common';
import { ExplorerNode, PageableExplorerNode, ResourceType } from './explorerNode';
export interface CommitsQueryResults {
label: string;
log: GitLog | undefined;
}
export class ResultsCommitsNode extends ExplorerNode implements PageableExplorerNode {
readonly supportsPaging: boolean = true;
maxCount: number | undefined;
constructor(
public readonly repoPath: string,
private readonly commitsQuery: (maxCount: number | undefined) => Promise<CommitsQueryResults>,
private readonly explorer: ResultsExplorer,
private readonly contextValue: ResourceType = ResourceType.ResultsCommits
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
const { log } = await this.getCommitsQueryResults();
if (log === undefined) return [];
const children: (CommitNode | ShowAllNode)[] = [
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))
];
if (log.truncated) {
children.push(new ShowAllNode('Results', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const { label, log } = await this.getCommitsQueryResults();
const item = new TreeItem(
label,
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None
);
item.contextValue = this.contextValue;
return item;
}
async refresh() {
this._commitsQueryResults = this.commitsQuery(this.maxCount);
}
private _commitsQueryResults: Promise<CommitsQueryResults> | undefined;
private getCommitsQueryResults() {
if (this._commitsQueryResults === undefined) {
this._commitsQueryResults = this.commitsQuery(this.maxCount);
}
return this._commitsQueryResults;
}
}

+ 84
- 0
src/views/nodes/resultsComparisonNode.ts Целия файл

@ -0,0 +1,84 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitService, GitUri } from '../../git/gitService';
import { Strings } from '../../system';
import { ResultsExplorer } from '../resultsExplorer';
import { ExplorerNode, NamedRef, ResourceType } from './explorerNode';
import { CommitsQueryResults, ResultsCommitsNode } from './resultsCommitsNode';
import { StatusFilesResultsNode } from './statusFilesResultsNode';
export class ResultsComparisonNode extends ExplorerNode {
constructor(
public readonly repoPath: string,
ref1: NamedRef,
ref2: NamedRef,
private readonly explorer: ResultsExplorer
) {
super(GitUri.fromRepoPath(repoPath));
this._ref1 = ref1;
this._ref2 = ref2;
}
private _ref1: NamedRef;
get ref1(): NamedRef {
return this._ref1;
}
private _ref2: NamedRef;
get ref2(): NamedRef {
return this._ref2;
}
async getChildren(): Promise<ExplorerNode[]> {
return [
new ResultsCommitsNode(this.uri.repoPath!, this.getCommitsQuery.bind(this), this.explorer),
new StatusFilesResultsNode(this.uri.repoPath!, this._ref1.ref, this._ref2.ref, this.explorer)
];
}
async getTreeItem(): Promise<TreeItem> {
let repository = '';
if ((await Container.git.getRepositoryCount()) > 1) {
const repo = await Container.git.getRepository(this.uri.repoPath!);
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`;
}
const item = new TreeItem(
`Comparing ${this._ref1.label ||
GitService.shortenSha(this._ref1.ref, { working: 'Working Tree' })} to ${this._ref2.label ||
GitService.shortenSha(this._ref2.ref, { working: 'Working Tree' })}${repository}`,
TreeItemCollapsibleState.Expanded
);
item.contextValue = ResourceType.ComparisonResults;
return item;
}
swap() {
const ref1 = this._ref1;
this._ref1 = this._ref2;
this._ref2 = ref1;
this.explorer.triggerNodeUpdate(this);
}
private async getCommitsQuery(maxCount: number | undefined): Promise<CommitsQueryResults> {
const log = await Container.git.getLog(this.uri.repoPath!, {
maxCount: maxCount,
ref: `${this._ref1.ref}...${this._ref2.ref || 'HEAD'}`
});
const count = log !== undefined ? log.count : 0;
const truncated = log !== undefined ? log.truncated : false;
const label = Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' });
return {
label: label,
log: log
};
}
}

+ 64
- 0
src/views/nodes/resultsNode.ts Целия файл

@ -0,0 +1,64 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ResultsExplorer } from '../resultsExplorer';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType, unknownGitUri } from './explorerNode';
export class ResultsNode extends ExplorerNode {
private _children: (ExplorerNode | MessageNode)[] = [];
constructor(
public readonly explorer: ResultsExplorer
) {
super(unknownGitUri);
}
async getChildren(): Promise<ExplorerNode[]> {
if (this._children.length === 0) return [new MessageNode('No results')];
return this._children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(`Results`, TreeItemCollapsibleState.Expanded);
item.contextValue = ResourceType.Results;
return item;
}
addOrReplace(results: ExplorerNode, replace: boolean) {
if (this._children.includes(results)) return;
if (this._children.length !== 0 && replace) {
this._children.length = 0;
this._children.push(results);
}
else {
this._children.splice(0, 0, results);
}
this.explorer.triggerNodeUpdate();
}
clear() {
if (this._children.length === 0) return;
this._children.length = 0;
this.explorer.triggerNodeUpdate();
}
dismiss(node: ExplorerNode) {
if (this._children.length === 0) return;
const index = this._children.findIndex(n => n === node);
if (index === -1) return;
this._children.splice(index, 1);
this.explorer.triggerNodeUpdate();
}
async refresh() {
if (this._children.length === 0) return;
this._children.forEach(c => c.refresh());
}
}

+ 2
- 1
src/views/nodes/stashFileNode.ts Целия файл

@ -1,7 +1,8 @@
'use strict';
import { GitLogCommit, IGitStatusFile } from '../../git/gitService';
import { Explorer } from '../explorer';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { Explorer, ResourceType } from './explorerNode';
import { ResourceType } from './explorerNode';
export class StashFileNode extends CommitFileNode {
constructor(status: IGitStatusFile, commit: GitLogCommit, explorer: Explorer) {

+ 7
- 1
src/views/nodes/stashNode.ts Целия файл

@ -3,7 +3,8 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { CommitFormatter, GitStashCommit, ICommitFormatOptions } from '../../git/gitService';
import { Iterables } from '../../system';
import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
import { Explorer } from '../explorer';
import { ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode';
import { StashFileNode } from './stashFileNode';
export class StashNode extends ExplorerRefNode {
@ -14,6 +15,10 @@ export class StashNode extends ExplorerRefNode {
super(commit.toGitUri());
}
get id(): string {
return `gitlens:repository(${this.commit.repoPath}):stash(${this.commit.sha})`;
}
get ref(): string {
return this.commit.sha;
}
@ -48,6 +53,7 @@ export class StashNode extends ExplorerRefNode {
} as ICommitFormatOptions),
TreeItemCollapsibleState.Collapsed
);
item.id = this.id;
item.contextValue = ResourceType.Stash;
item.tooltip = CommitFormatter.fromTemplate('${ago} (${date})\n\n${message}', this.commit, {
dateFormat: Container.config.defaultDateFormat

+ 6
- 4
src/views/nodes/stashesNode.ts Целия файл

@ -3,21 +3,22 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { GitUri, Repository } from '../../git/gitService';
import { Iterables } from '../../system';
import { Explorer, ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { Explorer } from '../explorer';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType } from './explorerNode';
import { StashNode } from './stashNode';
export class StashesNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: Explorer,
private readonly active: boolean = false
private readonly explorer: Explorer
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:stashes`;
return `gitlens:repository(${this.repo.path}):stashes`;
}
async getChildren(): Promise<ExplorerNode[]> {
@ -29,6 +30,7 @@ export class StashesNode extends ExplorerNode {
getTreeItem(): TreeItem {
const item = new TreeItem(`Stashes`, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ResourceType.Stashes;
item.iconPath = {

+ 2
- 1
src/views/nodes/statusFileCommitsNode.ts Целия файл

@ -13,8 +13,9 @@ import {
StatusFileFormatter
} from '../../git/gitService';
import { Strings } from '../../system';
import { Explorer } from '../explorer';
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { ExplorerNode, ResourceType } from './explorerNode';
export class StatusFileCommitsNode extends ExplorerNode {
constructor(

+ 9
- 2
src/views/nodes/statusFileNode.ts Целия файл

@ -3,8 +3,15 @@ import * as path from 'path';
import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithCommandArgs } from '../../commands';
import { Container } from '../../container';
import { getGitStatusIcon, GitStatusFile, GitUri, IStatusFormatOptions, StatusFileFormatter } from '../../git/gitService';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import {
getGitStatusIcon,
GitStatusFile,
GitUri,
IStatusFormatOptions,
StatusFileFormatter
} from '../../git/gitService';
import { Explorer } from '../explorer';
import { ExplorerNode, ResourceType } from './explorerNode';
export class StatusFileNode extends ExplorerNode {
constructor(

+ 40
- 98
src/views/nodes/statusFilesNode.ts Целия файл

@ -3,6 +3,7 @@ import * as path from 'path';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerFilesLayout } from '../../configuration';
import { Container } from '../../container';
import { GitStatusFile } from '../../git/git';
import {
GitCommitType,
GitLog,
@ -14,26 +15,24 @@ import {
} from '../../git/gitService';
import { Arrays, Iterables, Objects, Strings } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { StatusFileCommitsNode } from './statusFileCommitsNode';
export class StatusFilesNode extends ExplorerNode {
readonly repoPath: string;
readonly supportsPaging: boolean = true;
constructor(
public readonly status: GitStatus,
public readonly range: string | undefined,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
private readonly explorer: GitExplorer
) {
super(GitUri.fromRepoPath(status.repoPath));
this.repoPath = status.repoPath;
}
get id(): string {
return `gitlens:repository(${this.status.repoPath})${this.active ? ':active' : ''}:status:files`;
return `gitlens:repository(${this.status.repoPath}):status:files`;
}
async getChildren(): Promise<ExplorerNode[]> {
@ -43,15 +42,13 @@ export class StatusFilesNode extends ExplorerNode {
let log: GitLog | undefined;
if (this.range !== undefined) {
log = await Container.git.getLog(repoPath, { maxCount: this.maxCount, ref: this.range });
log = await Container.git.getLog(repoPath, { maxCount: 0, ref: this.range });
if (log !== undefined) {
statuses = Array.from(
Iterables.flatMap(log.commits.values(), c => {
return c.fileStatuses.map(s => {
return { ...s, commit: c } as IGitStatusFileWithCommit;
});
})
);
statuses = [
...Iterables.flatMap(log.commits.values(), c =>
c.fileStatuses.map(s => ({ ...s, commit: c } as IGitStatusFileWithCommit))
)
];
}
}
@ -66,91 +63,15 @@ export class StatusFilesNode extends ExplorerNode {
older.setMilliseconds(older.getMilliseconds() - 1);
return [
{
...s,
status: s.status,
commit: new GitLogCommit(
GitCommitType.File,
repoPath,
GitService.uncommittedSha,
'You',
undefined,
new Date(),
'',
s.fileName,
[s],
s.status,
s.originalFileName,
GitService.stagedUncommittedSha,
s.fileName
)
} as IGitStatusFileWithCommit,
{
...s,
status: s.status,
commit: new GitLogCommit(
GitCommitType.File,
repoPath,
GitService.stagedUncommittedSha,
'You',
undefined,
older,
'',
s.fileName,
[s],
s.status,
s.originalFileName,
'HEAD',
s.fileName
)
} as IGitStatusFileWithCommit
this.toStatusFile(s, GitService.uncommittedSha, GitService.stagedUncommittedSha),
this.toStatusFile(s, GitService.stagedUncommittedSha, 'HEAD', older)
];
}
else if (s.indexStatus !== undefined) {
return [
{
...s,
status: s.status,
commit: new GitLogCommit(
GitCommitType.File,
repoPath,
GitService.stagedUncommittedSha,
'You',
undefined,
new Date(),
'',
s.fileName,
[s],
s.status,
s.originalFileName,
'HEAD',
s.fileName
)
} as IGitStatusFileWithCommit
];
return [this.toStatusFile(s, GitService.stagedUncommittedSha, 'HEAD')];
}
else {
return [
{
...s,
status: s.status,
commit: new GitLogCommit(
GitCommitType.File,
repoPath,
GitService.uncommittedSha,
'You',
undefined,
new Date(),
'',
s.fileName,
[s],
s.status,
s.originalFileName,
'HEAD',
s.fileName
)
} as IGitStatusFileWithCommit
];
return [this.toStatusFile(s, GitService.uncommittedSha, 'HEAD')];
}
})
);
@ -188,11 +109,6 @@ export class StatusFilesNode extends ExplorerNode {
children.sort((a, b) => (a.priority ? -1 : 1) - (b.priority ? -1 : 1) || a.label!.localeCompare(b.label!));
}
if (log !== undefined && log.truncated) {
(children as (IFileExplorerNode | ShowAllNode)[]).push(
new ShowAllNode('Show All Changes', this, this.explorer)
);
}
return children;
}
@ -237,4 +153,30 @@ export class StatusFilesNode extends ExplorerNode {
private get includeWorkingTree(): boolean {
return this.explorer.config.includeWorkingTree;
}
private toStatusFile(s: GitStatusFile, ref: string, previousRef: string, date?: Date): IGitStatusFileWithCommit {
return {
status: s.status,
repoPath: s.repoPath,
indexStatus: s.indexStatus,
workTreeStatus: s.workTreeStatus,
fileName: s.fileName,
originalFileName: s.originalFileName,
commit: new GitLogCommit(
GitCommitType.File,
s.repoPath,
ref,
'You',
undefined,
date || new Date(),
'',
s.fileName,
[s],
s.status,
s.originalFileName,
previousRef,
s.fileName
)
};
}
}

+ 2
- 1
src/views/nodes/statusFilesResultsNode.ts Целия файл

@ -5,7 +5,8 @@ import { ExplorerFilesLayout } from '../../configuration';
import { Container } from '../../container';
import { GitStatusFile, GitUri } from '../../git/gitService';
import { Arrays, Iterables, Strings } from '../../system';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { Explorer } from '../explorer';
import { ExplorerNode, ResourceType } from './explorerNode';
import { FolderNode, IFileExplorerNode } from './folderNode';
import { StatusFileNode } from './statusFileNode';

+ 0
- 180
src/views/nodes/statusNode.ts Целия файл

@ -1,180 +0,0 @@
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitBranch, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../../git/gitService';
import { GitExplorer } from '../gitExplorer';
import { BranchNode } from './branchNode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { StatusFilesNode } from './statusFilesNode';
import { StatusUpstreamNode } from './statusUpstreamNode';
export class StatusNode extends ExplorerNode {
constructor(
uri: GitUri,
public readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:status`;
}
async getChildren(): Promise<ExplorerNode[]> {
this.resetChildren();
const children = [];
const status = await this.repo.getStatus();
if (status !== undefined) {
if (status.state.behind) {
children.push(new StatusUpstreamNode(status, 'behind', this.explorer, this.active));
}
if (status.state.ahead) {
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer, this.active));
}
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) {
const range = status.upstream ? `${status.upstream}..${status.ref}` : undefined;
children.push(new StatusFilesNode(status, range, this.explorer, this.active));
}
}
let branch = await this.repo.getBranch();
if (branch !== undefined) {
if (status !== undefined) {
branch = new GitBranch(
branch.repoPath,
branch.name,
branch.current,
branch.sha,
branch.tracking,
status.state.ahead,
status.state.behind,
branch.detached
);
}
children.push(new StatusBranchNode(branch, this.uri, this.explorer));
}
this.children = children;
return this.children;
}
async getTreeItem(): Promise<TreeItem> {
if (this.disposable !== undefined) {
this.disposable.dispose();
this.disposable = undefined;
}
const status = await this.repo.getStatus();
if (status === undefined) return new TreeItem('No repo status');
if (this.explorer.autoRefresh && this.includeWorkingTree) {
this.disposable = Disposable.from(
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this),
this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this),
{ dispose: () => this.repo.stopWatchingFileSystem() }
);
this.repo.startWatchingFileSystem();
}
let hasChildren = false;
const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree;
let label = `${status.getUpstreamStatus({ prefix: `${GlyphChars.Space} ` })}${
hasWorkingChanges ? status.getFormattedDiffStatus({ prefix: `${GlyphChars.Space} ` }) : ''
}`;
let tooltip = `${status.branch} (current)`;
let iconSuffix = '';
if (status.upstream) {
if (this.explorer.config.showTrackingBranch) {
label += `${GlyphChars.Space} ${GlyphChars.ArrowLeftRightLong}${GlyphChars.Space} ${status.upstream}`;
}
tooltip += `\n\nTracking ${GlyphChars.Dash} ${status.upstream}
${status.getUpstreamStatus({ empty: 'up-to-date', expand: true, separator: '\n' })}`;
if (status.state.ahead || status.state.behind) {
hasChildren = true;
if (status.state.behind) {
iconSuffix = '-red';
}
if (status.state.ahead) {
iconSuffix = status.state.behind ? '-yellow' : '-green';
}
}
}
if (hasWorkingChanges) {
tooltip += `\n\nHas uncommitted changes${status.getFormattedDiffStatus({
expand: true,
prefix: `\n`,
separator: '\n'
})}`;
}
let state: TreeItemCollapsibleState;
if (hasChildren || hasWorkingChanges) {
// HACK: Until https://github.com/Microsoft/vscode/issues/30918 is fixed
state = this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed;
}
else {
state = TreeItemCollapsibleState.Collapsed;
}
const item = new TreeItem(`${status.branch}${label}`, state);
item.id = this.id;
item.contextValue = ResourceType.Status;
item.tooltip = tooltip;
item.iconPath = {
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`),
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`)
};
return item;
}
private get includeWorkingTree(): boolean {
return this.explorer.config.includeWorkingTree;
}
private onAutoRefreshChanged() {
if (this.disposable === undefined) return;
// If auto-refresh changes, just kill the subscriptions
// (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here)
this.disposable.dispose();
this.disposable = undefined;
}
private async onFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
this.explorer.refreshNode(this);
}
}
export class StatusBranchNode extends BranchNode {
constructor(branch: GitBranch, uri: GitUri, explorer: GitExplorer) {
super(branch, uri, explorer);
}
get markCurrent() {
return false;
}
async getTreeItem(): Promise<TreeItem> {
const item = await super.getTreeItem();
if (item.label!.startsWith('(') && item.label!.endsWith(')')) {
item.label = `History ${item.label}`;
}
else {
item.label = `History (${item.label})`;
}
return item;
}
}

+ 36
- 24
src/views/nodes/statusUpstreamNode.ts Целия файл

@ -3,49 +3,61 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../../container';
import { GitStatus, GitUri } from '../../git/gitService';
import { Iterables, Strings } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { CommitNode } from './commitNode';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { ShowMoreNode } from './common';
import { ExplorerNode, PageableExplorerNode, ResourceType } from './explorerNode';
export class StatusUpstreamNode extends ExplorerNode implements PageableExplorerNode {
readonly supportsPaging: boolean = true;
maxCount: number | undefined;
export class StatusUpstreamNode extends ExplorerNode {
constructor(
public readonly status: GitStatus,
public readonly direction: 'ahead' | 'behind',
private readonly explorer: Explorer,
private readonly active: boolean = false
private readonly explorer: GitExplorer
) {
super(GitUri.fromRepoPath(status.repoPath));
}
get id(): string {
return `gitlens:repository(${this.status.repoPath})${this.active ? ':active' : ''}:status:upstream:${
this.direction
}`;
return `gitlens:repository(${this.status.repoPath}):status:upstream:${this.direction}`;
}
async getChildren(): Promise<ExplorerNode[]> {
const range =
this.direction === 'ahead'
? `${this.status.upstream}..${this.status.ref}`
: `${this.status.ref}..${this.status.upstream}`;
const ahead = this.direction === 'ahead';
const range = ahead
? `${this.status.upstream}..${this.status.ref}`
: `${this.status.ref}..${this.status.upstream}`;
let log = await Container.git.getLog(this.uri.repoPath!, { maxCount: 0, ref: range });
const log = await Container.git.getLog(this.uri.repoPath!, {
maxCount: this.maxCount || this.explorer.config.defaultItemLimit,
ref: range
});
if (log === undefined) return [];
if (this.direction !== 'ahead') {
return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))];
}
// Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up
const commits = Array.from(log.commits.values());
const commit = commits[commits.length - 1];
if (commit.previousSha === undefined) {
log = await Container.git.getLog(this.uri.repoPath!, { maxCount: 2, ref: commit.sha });
if (log !== undefined) {
commits[commits.length - 1] = Iterables.first(log.commits.values());
let children: (CommitNode | ShowMoreNode)[];
if (ahead) {
// Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up
const commits = [...log.commits.values()];
const commit = commits[commits.length - 1];
if (commit.previousSha === undefined) {
const previousLog = await Container.git.getLog(this.uri.repoPath!, { maxCount: 2, ref: commit.sha });
if (previousLog !== undefined) {
commits[commits.length - 1] = Iterables.first(previousLog.commits.values());
}
}
children = commits.map(c => new CommitNode(c, this.explorer));
}
else {
children = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))];
}
return [...Iterables.map(commits, c => new CommitNode(c, this.explorer))];
if (log.truncated) {
children.push(new ShowMoreNode('Commits', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {

+ 17
- 6
src/views/nodes/tagNode.ts Целия файл

@ -6,10 +6,12 @@ import { GitTag, GitUri } from '../../git/gitService';
import { Iterables } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { CommitNode } from './commitNode';
import { ExplorerNode, ExplorerRefNode, MessageNode, ResourceType, ShowAllNode } from './explorerNode';
import { MessageNode, ShowMoreNode } from './common';
import { ExplorerNode, ExplorerRefNode, PageableExplorerNode, ResourceType } from './explorerNode';
export class TagNode extends ExplorerRefNode {
export class TagNode extends ExplorerRefNode implements PageableExplorerNode {
readonly supportsPaging: boolean = true;
maxCount: number | undefined;
constructor(
public readonly tag: GitTag,
@ -19,6 +21,10 @@ export class TagNode extends ExplorerRefNode {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.tag.repoPath}):tag(${this.tag.name})`;
}
get label(): string {
return this.explorer.config.branches.layout === ExplorerBranchesLayout.Tree
? this.tag.getBasename()
@ -30,23 +36,28 @@ export class TagNode extends ExplorerRefNode {
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount, ref: this.tag.name });
const log = await Container.git.getLog(this.uri.repoPath!, {
maxCount: this.maxCount || this.explorer.config.defaultItemLimit,
ref: this.tag.name
});
if (log === undefined) return [new MessageNode('No commits yet')];
const children: (CommitNode | ShowAllNode)[] = [
const children: (CommitNode | ShowMoreNode)[] = [
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))
];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.explorer));
children.push(new ShowMoreNode('Commits', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.tooltip = `${this.tag.name}${this.tag.annotation === undefined ? '' : `\n${this.tag.annotation}`}`;
item.id = this.id;
item.contextValue = ResourceType.Tag;
item.tooltip = `${this.tag.name}${this.tag.annotation === undefined ? '' : `\n${this.tag.annotation}`}`;
return item;
}
}

+ 6
- 5
src/views/nodes/tagsNode.ts Целия файл

@ -6,21 +6,21 @@ import { GitUri, Repository } from '../../git/gitService';
import { Arrays } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { MessageNode } from './common';
import { ExplorerNode, ResourceType } from './explorerNode';
import { TagNode } from './tagNode';
export class TagsNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
private readonly explorer: GitExplorer
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:tags`;
return `gitlens:repository(${this.repo.path}):tags`;
}
async getChildren(): Promise<ExplorerNode[]> {
@ -38,13 +38,14 @@ export class TagsNode extends ExplorerNode {
this.explorer.config.files.compact
);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const root = new BranchOrTagFolderNode('tag', this.repo.path, '', undefined, hierarchy, this.explorer);
const children = (await root.getChildren()) as (BranchOrTagFolderNode | TagNode)[];
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Tags`, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ResourceType.Tags;
item.iconPath = {

+ 104
- 197
src/views/resultsExplorer.ts Целия файл

@ -1,83 +1,73 @@
'use strict';
import {
commands,
ConfigurationChangeEvent,
Disposable,
Event,
EventEmitter,
TreeDataProvider,
TreeItem,
TreeView,
window
} from 'vscode';
import { commands, ConfigurationChangeEvent } from 'vscode';
import { configuration, ExplorerFilesLayout, IExplorersConfig, IResultsExplorerConfig } from '../configuration';
import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { GitLog, GitLogCommit } from '../git/gitService';
import { Logger } from '../logger';
import { Functions, Strings } from '../system';
import { ExplorerBase, RefreshReason } from './explorer';
import { RefreshNodeCommandArgs } from './explorerCommands';
import {
CommitResultsNode,
CommitsResultsNode,
ComparisonResultsNode,
ExplorerNode,
MessageNode,
NamedRef,
RefreshReason,
ResourceType
ResourceType,
ResultsCommitNode,
ResultsCommitsNode,
ResultsComparisonNode,
ResultsNode
} from './nodes';
// import { Messages } from './messages';
export * from './nodes';
export class ResultsExplorer extends ExplorerBase<ResultsNode> {
constructor() {
super('gitlens.resultsExplorer');
export class ResultsExplorer implements TreeDataProvider<ExplorerNode>, Disposable {
private _disposable: Disposable | undefined;
private _roots: ExplorerNode[] = [];
private _tree: TreeView<ExplorerNode> | undefined;
setCommandContext(CommandContext.ResultsExplorerKeepResults, this.keepResults);
}
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>();
public get onDidChangeTreeData(): Event<ExplorerNode> {
return this._onDidChangeTreeData.event;
getRoot() {
return new ResultsNode(this);
}
constructor() {
protected registerCommands() {
Container.explorerCommands;
commands.registerCommand('gitlens.resultsExplorer.refresh', this.refreshNodes, this);
commands.registerCommand('gitlens.resultsExplorer.refreshNode', this.refreshNode, this);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this);
commands.registerCommand(
'gitlens.resultsExplorer.setFilesLayoutToAuto',
this.getQualifiedCommand('refreshNode'),
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args),
this
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToAuto'),
() => this.setFilesLayout(ExplorerFilesLayout.Auto),
this
);
commands.registerCommand(
'gitlens.resultsExplorer.setFilesLayoutToList',
this.getQualifiedCommand('setFilesLayoutToList'),
() => this.setFilesLayout(ExplorerFilesLayout.List),
this
);
commands.registerCommand(
'gitlens.resultsExplorer.setFilesLayoutToTree',
this.getQualifiedCommand('setFilesLayoutToTree'),
() => this.setFilesLayout(ExplorerFilesLayout.Tree),
this
);
commands.registerCommand('gitlens.resultsExplorer.clearResultsNode', this.clearResultsNode, this);
commands.registerCommand('gitlens.resultsExplorer.close', this.close, this);
commands.registerCommand('gitlens.resultsExplorer.setKeepResultsToOn', () => this.setKeepResults(true), this);
commands.registerCommand('gitlens.resultsExplorer.setKeepResultsToOff', () => this.setKeepResults(false), this);
commands.registerCommand('gitlens.resultsExplorer.swapComparision', this.swapComparision, this);
setCommandContext(CommandContext.ResultsExplorerKeepResults, this.keepResults);
Container.context.subscriptions.push(configuration.onDidChange(this.onConfigurationChanged, this));
void this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._disposable && this._disposable.dispose();
commands.registerCommand(
this.getQualifiedCommand('dismissNode'),
(node: ExplorerNode) => this.dismissNode(node),
this
);
commands.registerCommand(this.getQualifiedCommand('close'), () => this.close(), this);
commands.registerCommand(this.getQualifiedCommand('setKeepResultsToOn'), () => this.setKeepResults(true), this);
commands.registerCommand(
this.getQualifiedCommand('setKeepResultsToOff'),
() => this.setKeepResults(false),
this
);
commands.registerCommand(this.getQualifiedCommand('swapComparision'), this.swapComparision, this);
}
private async onConfigurationChanged(e: ConfigurationChangeEvent) {
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
const initializing = configuration.initializing(e);
if (
@ -94,18 +84,10 @@ export class ResultsExplorer implements TreeDataProvider, Disposab
}
if (initializing || configuration.changed(e, configuration.name('resultsExplorer')('location').value)) {
if (this._disposable) {
this._disposable.dispose();
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>();
}
this._tree = window.createTreeView(`gitlens.resultsExplorer:${this.config.location}`, {
treeDataProvider: this
});
this._disposable = this._tree;
this.initialize(this.config.location);
}
if (!initializing && this._roots.length !== 0) {
if (!initializing && this._root !== undefined) {
void this.refresh(RefreshReason.ConfigurationChanged);
}
}
@ -124,90 +106,30 @@ export class ResultsExplorer implements TreeDataProvider, Disposab
}
close() {
this.clearResults();
if (this._root === undefined) return;
this._root.clear();
this._enabled = false;
setCommandContext(CommandContext.ResultsExplorer, false);
}
getParent(element: ExplorerNode): ExplorerNode | undefined {
return undefined;
}
async getChildren(node?: ExplorerNode): Promise<ExplorerNode[]> {
if (this._roots.length === 0) return [new MessageNode('No results')];
if (node === undefined) return this._roots;
return node.getChildren();
}
async getTreeItem(node: ExplorerNode): Promise<TreeItem> {
return node.getTreeItem();
}
getQualifiedCommand(command: string) {
return `gitlens.resultsExplorer.${command}`;
}
async refresh(reason?: RefreshReason) {
if (reason === undefined) {
reason = RefreshReason.Command;
}
Logger.log(`ResultsExplorer.refresh`, `reason='${reason}'`);
this._onDidChangeTreeData.fire();
}
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) {
Logger.log(`ResultsExplorer.refreshNode(${(node as { id?: string }).id || ''})`);
if (args !== undefined && node.supportsPaging) {
node.maxCount = args.maxCount;
}
node.refresh();
// Since a root node won't actually refresh, force everything
this._onDidChangeTreeData.fire(this._roots.includes(node) ? undefined : node);
}
refreshNodes() {
Logger.log(`ResultsExplorer.refreshNodes`);
this._roots.forEach(n => n.refresh());
this._onDidChangeTreeData.fire();
}
async show() {
if (this._roots === undefined || this._roots.length === 0 || this._tree === undefined) return;
try {
await this._tree.reveal(this._roots[0], { select: false });
}
catch (ex) {
Logger.error(ex);
}
addCommit(commit: GitLogCommit) {
return this.addResults(new ResultsCommitNode(commit, this));
}
showComparisonInResults(repoPath: string, ref1: string | NamedRef, ref2: string | NamedRef) {
void this.showResults(
this.addResults(
new ComparisonResultsNode(
repoPath,
typeof ref1 === 'string' ? { ref: ref1 } : ref1,
typeof ref2 === 'string' ? { ref: ref2 } : ref2,
this
)
addComparison(repoPath: string, ref1: string | NamedRef, ref2: string | NamedRef) {
return this.addResults(
new ResultsComparisonNode(
repoPath,
typeof ref1 === 'string' ? { ref: ref1 } : ref1,
typeof ref2 === 'string' ? { ref: ref2 } : ref2,
this
)
);
}
showCommitInResults(commit: GitLogCommit) {
void this.showResults(this.addResults(new CommitResultsNode(commit, this)));
}
showCommitsInResults(
addSearchResults(
results: GitLog,
resultsLabel:
| string
@ -216,84 +138,69 @@ export class ResultsExplorer implements TreeDataProvider, Disposab
resultsType?: { singular: string; plural: string };
}
) {
const query =
results.query === undefined ? (maxCount: number | undefined) => Promise.resolve(results) : results.query;
const labelFn = async (log: GitLog | undefined) => {
if (typeof resultsLabel === 'string') return resultsLabel;
const count = log !== undefined ? log.count : 0;
const truncated = log !== undefined ? log.truncated : false;
const resultsType =
resultsLabel.resultsType === undefined
? { singular: 'result', plural: 'results' }
: resultsLabel.resultsType;
let repository = '';
if ((await Container.git.getRepositoryCount()) > 1) {
const repo = await Container.git.getRepository(results.repoPath);
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) ||
results.repoPath}`;
const getCommitsQuery = async (maxCount: number | undefined) => {
const log = await Functions.seeded(
results.query === undefined
? (maxCount: number | undefined) => Promise.resolve(results)
: results.query,
results
)(maxCount);
let label;
if (typeof resultsLabel === 'string') {
label = resultsLabel;
}
else {
const count = log !== undefined ? log.count : 0;
const truncated = log !== undefined ? log.truncated : false;
const resultsType =
resultsLabel.resultsType === undefined
? { singular: 'result', plural: 'results' }
: resultsLabel.resultsType;
let repository = '';
if ((await Container.git.getRepositoryCount()) > 1) {
const repo = await Container.git.getRepository(results.repoPath);
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) ||
results.repoPath}`;
}
label = `${Strings.pluralize(resultsType.singular, count, {
number: truncated ? `${count}+` : undefined,
plural: resultsType.plural,
zero: 'No'
})} for ${resultsLabel.label}${repository}`;
}
return `${Strings.pluralize(resultsType.singular, count, {
number: truncated ? `${count}+` : undefined,
plural: resultsType.plural,
zero: 'No'
})} for ${resultsLabel.label}${repository}`;
return {
label: label,
log: log
};
};
void this.showResults(
this.addResults(
new CommitsResultsNode(
results.repoPath,
labelFn,
Functions.seeded(query, results),
this,
ResourceType.SearchResults
)
)
return this.addResults(
new ResultsCommitsNode(results.repoPath, getCommitsQuery, this, ResourceType.SearchResults)
);
}
private async showResults(results: ExplorerNode) {
this._enabled = true;
await setCommandContext(CommandContext.ResultsExplorer, this.config.location);
setTimeout(() => this._tree!.reveal(results, { select: true }), 250);
}
private addResults(results: ExplorerNode): ExplorerNode {
if (this._roots.includes(results)) return results;
if (this._roots.length > 0 && !this.keepResults) {
this.clearResults();
private async addResults(results: ExplorerNode) {
if (this._root === undefined) {
this._root = this.getRoot();
}
this._roots.splice(0, 0, results);
this.refreshNode(results);
return results;
}
private clearResults() {
if (this._roots.length === 0) return;
this._root.addOrReplace(results, !this.keepResults);
this._roots.forEach(r => r.dispose());
this._roots = [];
this._enabled = true;
await setCommandContext(CommandContext.ResultsExplorer, this.config.location);
void this.refresh();
setTimeout(() => this._tree!.reveal(results, { select: true }), 250);
}
private clearResultsNode(node: ExplorerNode) {
const index = this._roots.findIndex(n => n === node);
if (index === -1) return;
this._roots.splice(index, 1);
node.dispose();
private dismissNode(node: ExplorerNode) {
if (this._root === undefined) return;
void this.refresh();
this._root.dismiss(node);
}
private setFilesLayout(layout: ExplorerFilesLayout) {
@ -306,8 +213,8 @@ export class ResultsExplorer implements TreeDataProvider, Disposab
}
private swapComparision(node: ExplorerNode) {
if (!(node instanceof ComparisonResultsNode)) return;
if (!(node instanceof ResultsComparisonNode)) return;
this.showComparisonInResults(node.repoPath, node.ref2, node.ref1);
node.swap();
}
}

Зареждане…
Отказ
Запис